mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
RunContext refactor + sync updates
This commit is contained in:
parent
c7cddb72c2
commit
d3cc11659a
79 changed files with 1415 additions and 626 deletions
272
.cursor/plans/script_naming_refactor_424e3277.plan.md
Normal file
272
.cursor/plans/script_naming_refactor_424e3277.plan.md
Normal file
|
|
@ -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
|
||||||
|
|
||||||
|
|
@ -8,6 +8,12 @@
|
||||||
- All other instances -> .model.json files or directories
|
- All other instances -> .model.json files or directories
|
||||||
|
|
||||||
Children are recursively encoded and included.
|
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
|
local Packages = script.Parent.Parent.Parent.Packages
|
||||||
|
|
@ -41,9 +47,65 @@ local DIRECTORY_CLASSES = {
|
||||||
AdGui = true,
|
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
|
-- Forward declaration for recursion
|
||||||
local encodeInstance
|
local encodeInstance
|
||||||
|
|
||||||
|
-- Track skipped duplicates for logging
|
||||||
|
local skippedDuplicateCount = 0
|
||||||
|
|
||||||
-- Encode Attributes if present on any instance
|
-- Encode Attributes if present on any instance
|
||||||
local function encodeAttributes(instance, properties)
|
local function encodeAttributes(instance, properties)
|
||||||
-- Try to get Attributes - this works for all instance types
|
-- Try to get Attributes - this works for all instance types
|
||||||
|
|
@ -64,7 +126,21 @@ local function encodeAttributes(instance, properties)
|
||||||
end
|
end
|
||||||
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 = {}
|
local properties = {}
|
||||||
|
|
||||||
-- Always try to encode Attributes for any instance type
|
-- Always try to encode Attributes for any instance type
|
||||||
|
|
@ -111,10 +187,25 @@ encodeInstance = function(instance, parentId)
|
||||||
end
|
end
|
||||||
-- For directory classes (Folder, Configuration, etc.), we only encode Attributes (done above)
|
-- 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 = {}
|
local children = {}
|
||||||
for _, child in ipairs(instance:GetChildren()) do
|
local childInstances = instance:GetChildren()
|
||||||
local encodedChild = encodeInstance(child, nil) -- parentId will be resolved server-side
|
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
|
if encodedChild then
|
||||||
table.insert(children, encodedChild)
|
table.insert(children, encodedChild)
|
||||||
end
|
end
|
||||||
|
|
@ -129,4 +220,20 @@ encodeInstance = function(instance, parentId)
|
||||||
}
|
}
|
||||||
end
|
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
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,10 @@ expression: contents
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">client_in_folder</string>
|
<string name="Name">client_in_folder</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
<Item class="LocalScript" referent="1">
|
<Item class="Script" referent="1">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">localScript</string>
|
<string name="Name">localScript</string>
|
||||||
|
<token name="RunContext">2</token>
|
||||||
<string name="Source">-- This is a Lua client script</string>
|
<string name="Source">-- This is a Lua client script</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ source: tests/tests/build.rs
|
||||||
expression: contents
|
expression: contents
|
||||||
---
|
---
|
||||||
<roblox version="4">
|
<roblox version="4">
|
||||||
<Item class="LocalScript" referent="0">
|
<Item class="Script" referent="0">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">client_init</string>
|
<string name="Name">client_init</string>
|
||||||
|
<token name="RunContext">2</token>
|
||||||
<string name="Source">return "From folder/init.client.lua"</string>
|
<string name="Source">return "From folder/init.client.lua"</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
---
|
---
|
||||||
source: tests/tests/build.rs
|
source: tests/tests/build.rs
|
||||||
assertion_line: 98
|
|
||||||
expression: contents
|
expression: contents
|
||||||
---
|
---
|
||||||
<roblox version="4">
|
<roblox version="4">
|
||||||
<Item class="LocalScript" referent="0">
|
<Item class="Script" referent="0">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">issue_546</string>
|
<string name="Name">issue_546</string>
|
||||||
<bool name="Disabled">true</bool>
|
<bool name="Disabled">true</bool>
|
||||||
|
<token name="RunContext">2</token>
|
||||||
<string name="Source">print("Hello, world!")</string>
|
<string name="Source">print("Hello, world!")</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ expression: contents
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">hello</string>
|
<string name="Name">hello</string>
|
||||||
<bool name="Disabled">true</bool>
|
<bool name="Disabled">true</bool>
|
||||||
<token name="RunContext">0</token>
|
<token name="RunContext">1</token>
|
||||||
<string name="Source">-- This script should be marked 'Disabled'</string>
|
<string name="Source">-- This script should be marked 'Disabled'</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ expression: contents
|
||||||
<Item class="Script" referent="1">
|
<Item class="Script" referent="1">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">serverScript</string>
|
<string name="Name">serverScript</string>
|
||||||
<token name="RunContext">0</token>
|
<token name="RunContext">1</token>
|
||||||
<string name="Source">-- This is a Lua server script</string>
|
<string name="Source">-- This is a Lua server script</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ expression: contents
|
||||||
<Item class="Script" referent="0">
|
<Item class="Script" referent="0">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">server_init</string>
|
<string name="Name">server_init</string>
|
||||||
<token name="RunContext">0</token>
|
<token name="RunContext">1</token>
|
||||||
<string name="Source">return "From folder/init.server.luau"</string>
|
<string name="Source">return "From folder/init.server.luau"</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
source: tests/tests/build.rs
|
source: tests/tests/build.rs
|
||||||
assertion_line: 104
|
|
||||||
expression: contents
|
expression: contents
|
||||||
---
|
---
|
||||||
<roblox version="4">
|
<roblox version="4">
|
||||||
|
|
@ -11,13 +10,14 @@ expression: contents
|
||||||
<Item class="Script" referent="1">
|
<Item class="Script" referent="1">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">bar</string>
|
<string name="Name">bar</string>
|
||||||
<token name="RunContext">0</token>
|
<token name="RunContext">1</token>
|
||||||
<string name="Source">-- Hello, from bar (a Script)!</string>
|
<string name="Source">-- Hello, from bar (a Script)!</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
<Item class="LocalScript" referent="2">
|
<Item class="Script" referent="2">
|
||||||
<Properties>
|
<Properties>
|
||||||
<string name="Name">baz</string>
|
<string name="Name">baz</string>
|
||||||
|
<token name="RunContext">2</token>
|
||||||
<string name="Source">-- Hello, from baz (a LocalScript)!</string>
|
<string name="Source">-- Hello, from baz (a LocalScript)!</string>
|
||||||
</Properties>
|
</Properties>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"name": "nested_runcontext",
|
|
||||||
"emitLegacyScripts": false,
|
|
||||||
"tree": {
|
|
||||||
"$className": "Folder",
|
|
||||||
"folder1": {
|
|
||||||
"$path": "folder1"
|
|
||||||
},
|
|
||||||
"folder2": {
|
|
||||||
"$path": "folder2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"name": "nested_runcontext",
|
|
||||||
"emitLegacyScripts": true,
|
|
||||||
"tree": {
|
|
||||||
"$path": "folder3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
|
|
||||||
---
|
---
|
||||||
instances:
|
instances:
|
||||||
id-2:
|
id-2:
|
||||||
|
|
@ -25,7 +24,7 @@ instances:
|
||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties:
|
Properties:
|
||||||
RunContext:
|
RunContext:
|
||||||
Enum: 0
|
Enum: 1
|
||||||
Source:
|
Source:
|
||||||
String: "-- Hello, from bar!"
|
String: "-- Hello, from bar!"
|
||||||
id-4:
|
id-4:
|
||||||
|
|
@ -41,4 +40,3 @@ instances:
|
||||||
String: Updated foo!
|
String: Updated foo!
|
||||||
messageCursor: 1
|
messageCursor: 1
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
|
|
||||||
---
|
---
|
||||||
instances:
|
instances:
|
||||||
id-2:
|
id-2:
|
||||||
|
|
@ -25,7 +24,7 @@ instances:
|
||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties:
|
Properties:
|
||||||
RunContext:
|
RunContext:
|
||||||
Enum: 0
|
Enum: 1
|
||||||
Source:
|
Source:
|
||||||
String: "-- Hello, from bar!"
|
String: "-- Hello, from bar!"
|
||||||
id-4:
|
id-4:
|
||||||
|
|
@ -41,4 +40,3 @@ instances:
|
||||||
String: "-- Hello, from foo!"
|
String: "-- Hello, from foo!"
|
||||||
messageCursor: 0
|
messageCursor: 0
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
source: tests/tests/serve.rs
|
source: tests/tests/serve.rs
|
||||||
assertion_line: 284
|
|
||||||
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
expression: "read_response.intern_and_redact(&mut redactions, root_id)"
|
||||||
---
|
---
|
||||||
instances:
|
instances:
|
||||||
|
|
@ -28,18 +27,20 @@ instances:
|
||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties:
|
Properties:
|
||||||
RunContext:
|
RunContext:
|
||||||
Enum: 0
|
Enum: 1
|
||||||
Source:
|
Source:
|
||||||
String: "-- Hello, from bar (a Script)!"
|
String: "-- Hello, from bar (a Script)!"
|
||||||
id-4:
|
id-4:
|
||||||
Children: []
|
Children: []
|
||||||
ClassName: LocalScript
|
ClassName: Script
|
||||||
Id: id-4
|
Id: id-4
|
||||||
Metadata:
|
Metadata:
|
||||||
ignoreUnknownInstances: false
|
ignoreUnknownInstances: false
|
||||||
Name: baz
|
Name: baz
|
||||||
Parent: id-2
|
Parent: id-2
|
||||||
Properties:
|
Properties:
|
||||||
|
RunContext:
|
||||||
|
Enum: 2
|
||||||
Source:
|
Source:
|
||||||
String: "-- Hello, from baz (a LocalScript)!"
|
String: "-- Hello, from baz (a LocalScript)!"
|
||||||
id-5:
|
id-5:
|
||||||
|
|
@ -77,4 +78,3 @@ instances:
|
||||||
String: "Hello, from qux (a .rojo file that's turned into a StringValue)!"
|
String: "Hello, from qux (a .rojo file that's turned into a StringValue)!"
|
||||||
messageCursor: 0
|
messageCursor: 0
|
||||||
sessionId: id-1
|
sessionId: id-1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.
|
//! Characters are encoded using a %NAME% format (e.g., `<` becomes `%LT%`,
|
||||||
//!
|
//! `.` becomes `%DOT%`). Literal `%` is escaped as `%%` (like printf).
|
||||||
//! This module provides functions to encode these characters using a %NAME%
|
|
||||||
//! format (e.g., `<` becomes `%LT%`) and decode them back.
|
|
||||||
|
|
||||||
/// 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)] = &[
|
const CHAR_ENCODINGS: &[(&str, &str)] = &[
|
||||||
|
(".", "%DOT%"),
|
||||||
("<", "%LT%"),
|
("<", "%LT%"),
|
||||||
(">", "%GT%"),
|
(">", "%GT%"),
|
||||||
(":", "%COLON%"),
|
(":", "%COLON%"),
|
||||||
|
|
@ -21,13 +26,27 @@ const CHAR_ENCODINGS: &[(&str, &str)] = &[
|
||||||
("*", "%STAR%"),
|
("*", "%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<HashMap<&'static str, &'static str>> = 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%";
|
const SPACE_ENCODING: &str = "%SPACE%";
|
||||||
|
|
||||||
/// Encodes Windows-invalid characters in a file name.
|
/// Encodes special characters in a file name.
|
||||||
///
|
///
|
||||||
/// This replaces characters like `<`, `>`, `:`, `"`, `/`, `\`, `|`, `?`, `*`
|
/// This replaces characters like `.`, `<`, `>`, `:`, `"`, `/`, `\`, `|`, `?`, `*`
|
||||||
/// with their encoded representations like `%LT%`, `%GT%`, etc.
|
/// with their encoded representations like `%DOT%`, `%LT%`, `%GT%`, etc.
|
||||||
/// Leading and trailing spaces are encoded as `%SPACE%`.
|
/// Literal `%` is escaped as `%%`. Leading and trailing spaces are encoded as `%SPACE%`.
|
||||||
pub fn encode_path_name(name: &str) -> String {
|
pub fn encode_path_name(name: &str) -> String {
|
||||||
// Count leading and trailing spaces first (before any modifications)
|
// Count leading and trailing spaces first (before any modifications)
|
||||||
let leading_spaces = name.len() - name.trim_start_matches(' ').len();
|
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)
|
// Get the middle part (without leading/trailing spaces)
|
||||||
let middle = &name[leading_spaces..name.len() - trailing_spaces];
|
let middle = &name[leading_spaces..name.len() - trailing_spaces];
|
||||||
|
|
||||||
// Encode invalid characters in the middle part
|
// First: escape % as %% (before adding new % signs from encodings)
|
||||||
let mut encoded_middle = middle.to_string();
|
let mut encoded_middle = middle.replace('%', "%%");
|
||||||
|
|
||||||
|
// Then: encode special characters
|
||||||
for (char, encoded) in CHAR_ENCODINGS {
|
for (char, encoded) in CHAR_ENCODINGS {
|
||||||
encoded_middle = encoded_middle.replace(char, encoded);
|
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)
|
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
|
/// This replaces encoded representations like `%DOT%`, `%LT%`, `%GT%`, etc.
|
||||||
/// to their original characters `<`, `>`, etc.
|
/// back to their original characters `.`, `<`, `>`, etc.
|
||||||
/// `%SPACE%` at the start/end is decoded back to spaces.
|
/// `%%` 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 {
|
pub fn decode_path_name(name: &str) -> String {
|
||||||
let mut result = name.to_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();
|
result = result[..result.len() - SPACE_ENCODING.len()].to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode invalid characters in the middle
|
// Parse and decode the middle part
|
||||||
for (char, encoded) in CHAR_ENCODINGS {
|
let decoded_middle = decode_patterns(&result);
|
||||||
result = result.replace(encoded, char);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rebuild with actual spaces
|
// Rebuild with actual spaces
|
||||||
let prefix = " ".repeat(leading_spaces);
|
let prefix = " ".repeat(leading_spaces);
|
||||||
let suffix = " ".repeat(trailing_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<char> = 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)]
|
#[cfg(test)]
|
||||||
|
|
@ -88,15 +179,15 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encode_all_chars() {
|
fn test_encode_all_chars() {
|
||||||
let input = r#"<>:"/\|?*"#;
|
let input = r#".<>:"/\|?*"#;
|
||||||
let expected = "%LT%%GT%%COLON%%QUOTE%%SLASH%%BACKSLASH%%PIPE%%QUESTION%%STAR%";
|
let expected = "%DOT%%LT%%GT%%COLON%%QUOTE%%SLASH%%BACKSLASH%%PIPE%%QUESTION%%STAR%";
|
||||||
assert_eq!(encode_path_name(input), expected);
|
assert_eq!(encode_path_name(input), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_decode_all_chars() {
|
fn test_decode_all_chars() {
|
||||||
let input = "%LT%%GT%%COLON%%QUOTE%%SLASH%%BACKSLASH%%PIPE%%QUESTION%%STAR%";
|
let input = "%DOT%%LT%%GT%%COLON%%QUOTE%%SLASH%%BACKSLASH%%PIPE%%QUESTION%%STAR%";
|
||||||
let expected = r#"<>:"/\|?*"#;
|
let expected = r#".<>:"/\|?*"#;
|
||||||
assert_eq!(decode_path_name(input), expected);
|
assert_eq!(decode_path_name(input), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,4 +245,67 @@ mod tests {
|
||||||
let decoded = decode_path_name(&encoded);
|
let decoded = decode_path_name(&encoded);
|
||||||
assert_eq!(decoded, original);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,12 +110,6 @@ pub struct Project {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub serve_address: Option<IpAddr>,
|
pub serve_address: Option<IpAddr>,
|
||||||
|
|
||||||
/// 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<bool>,
|
|
||||||
|
|
||||||
/// A list of globs, relative to the folder the project file is in, that
|
/// A list of globs, relative to the folder the project file is in, that
|
||||||
/// match files that should be excluded if Rojo encounters them.
|
/// match files that should be excluded if Rojo encounters them.
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
|
@ -573,7 +567,6 @@ mod test {
|
||||||
"$path": "src", // Comment after value
|
"$path": "src", // Comment after value
|
||||||
},
|
},
|
||||||
"servePort": 34567,
|
"servePort": 34567,
|
||||||
"emitLegacyScripts": false,
|
|
||||||
// Test glob parsing with comments
|
// Test glob parsing with comments
|
||||||
"globIgnorePaths": [
|
"globIgnorePaths": [
|
||||||
"**/*.spec.lua", // Ignore test files
|
"**/*.spec.lua", // Ignore test files
|
||||||
|
|
@ -601,7 +594,6 @@ mod test {
|
||||||
// Verify the parsed values
|
// Verify the parsed values
|
||||||
assert_eq!(project.name, Some("TestProject".to_string()));
|
assert_eq!(project.name, Some("TestProject".to_string()));
|
||||||
assert_eq!(project.serve_port, Some(34567));
|
assert_eq!(project.serve_port, Some(34567));
|
||||||
assert_eq!(project.emit_legacy_scripts, Some(false));
|
|
||||||
|
|
||||||
// Verify glob_ignore_paths were parsed correctly
|
// Verify glob_ignore_paths were parsed correctly
|
||||||
assert_eq!(project.glob_ignore_paths.len(), 2);
|
assert_eq!(project.glob_ignore_paths.len(), 2);
|
||||||
|
|
|
||||||
|
|
@ -110,10 +110,9 @@ impl ServeSession {
|
||||||
.syncback_rules
|
.syncback_rules
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|rules| rules.encode_windows_invalid_chars())
|
.map(|rules| rules.encode_windows_invalid_chars())
|
||||||
.unwrap_or(false);
|
.unwrap_or(true);
|
||||||
|
|
||||||
let mut instance_context =
|
let mut instance_context = InstanceContext::new();
|
||||||
InstanceContext::with_emit_legacy_scripts(root_project.emit_legacy_scripts);
|
|
||||||
instance_context.set_decode_windows_invalid_chars(decode_windows_invalid_chars);
|
instance_context.set_decode_windows_invalid_chars(decode_windows_invalid_chars);
|
||||||
|
|
||||||
log::trace!("Generating snapshot of instances from VFS");
|
log::trace!("Generating snapshot of instances from VFS");
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,7 @@ use anyhow::Context;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
glob::Glob,
|
glob::Glob, path_serializer, project::ProjectNode, snapshot_middleware::Middleware, RojoRef,
|
||||||
path_serializer,
|
|
||||||
project::ProjectNode,
|
|
||||||
snapshot_middleware::{emit_legacy_scripts_default, Middleware},
|
|
||||||
RojoRef,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Rojo-specific metadata that can be associated with an instance or a snapshot
|
/// Rojo-specific metadata that can be associated with an instance or a snapshot
|
||||||
|
|
@ -142,32 +138,26 @@ impl Default for InstanceMetadata {
|
||||||
pub struct InstanceContext {
|
pub struct InstanceContext {
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
pub path_ignore_rules: Arc<Vec<PathIgnoreRule>>,
|
pub path_ignore_rules: Arc<Vec<PathIgnoreRule>>,
|
||||||
pub emit_legacy_scripts: bool,
|
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
pub sync_rules: Vec<SyncRule>,
|
pub sync_rules: Vec<SyncRule>,
|
||||||
/// Whether to decode Windows-invalid characters in file names.
|
/// Whether to decode special characters in file names.
|
||||||
/// Characters like `%LT%`, `%GT%`, `%COLON%`, etc. will be decoded
|
/// Characters like `%DOT%`, `%LT%`, `%GT%`, `%COLON%`, etc. will be decoded
|
||||||
/// back to `<`, `>`, `:`, etc. in instance names sent to the plugin.
|
/// back to `.`, `<`, `>`, `:`, etc. in instance names sent to the plugin.
|
||||||
#[serde(default)]
|
/// Defaults to true.
|
||||||
|
#[serde(default = "decode_windows_invalid_chars_default")]
|
||||||
pub decode_windows_invalid_chars: bool,
|
pub decode_windows_invalid_chars: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decode_windows_invalid_chars_default() -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
impl InstanceContext {
|
impl InstanceContext {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
path_ignore_rules: Arc::new(Vec::new()),
|
path_ignore_rules: Arc::new(Vec::new()),
|
||||||
emit_legacy_scripts: emit_legacy_scripts_default().unwrap(),
|
|
||||||
sync_rules: Vec::new(),
|
sync_rules: Vec::new(),
|
||||||
decode_windows_invalid_chars: false,
|
decode_windows_invalid_chars: true,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_emit_legacy_scripts(emit_legacy_scripts: Option<bool>) -> Self {
|
|
||||||
Self {
|
|
||||||
emit_legacy_scripts: emit_legacy_scripts
|
|
||||||
.or_else(emit_legacy_scripts_default)
|
|
||||||
.unwrap(),
|
|
||||||
..Self::new()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,10 +196,6 @@ impl InstanceContext {
|
||||||
self.sync_rules.clear();
|
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
|
/// Returns the middleware specified by the first sync rule that
|
||||||
/// matches the provided path. This does not handle default syncing rules.
|
/// matches the provided path. This does not handle default syncing rules.
|
||||||
pub fn get_user_sync_rule(&self, path: &Path) -> Option<&SyncRule> {
|
pub fn get_user_sync_rule(&self, path: &Path) -> Option<&SyncRule> {
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@ metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,7 @@ metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ added_instances:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -383,7 +383,7 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#,
|
||||||
let vfs = Vfs::new(imfs);
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
let instance_snapshot = snapshot_csv_init(
|
let instance_snapshot = snapshot_csv_init(
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
&InstanceContext::new(),
|
||||||
&vfs,
|
&vfs,
|
||||||
Path::new("/root/init.csv"),
|
Path::new("/root/init.csv"),
|
||||||
"root",
|
"root",
|
||||||
|
|
@ -421,7 +421,7 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#,
|
||||||
let vfs = Vfs::new(imfs);
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
let instance_snapshot = snapshot_csv_init(
|
let instance_snapshot = snapshot_csv_init(
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
&InstanceContext::new(),
|
||||||
&vfs,
|
&vfs,
|
||||||
Path::new("/root/init.csv"),
|
Path::new("/root/init.csv"),
|
||||||
"root",
|
"root",
|
||||||
|
|
|
||||||
|
|
@ -21,14 +21,12 @@ use super::{
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ScriptType {
|
pub enum ScriptType {
|
||||||
Server,
|
Server, // Script + RunContext.Server
|
||||||
Client,
|
Client, // Script + RunContext.Client
|
||||||
Module,
|
Module, // ModuleScript
|
||||||
Plugin,
|
Plugin, // Script + RunContext.Plugin
|
||||||
LegacyServer,
|
Legacy, // Script + RunContext.Legacy
|
||||||
LegacyClient,
|
Local, // LocalScript
|
||||||
RunContextServer,
|
|
||||||
RunContextClient,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Core routine for turning Lua files into snapshots.
|
/// Core routine for turning Lua files into snapshots.
|
||||||
|
|
@ -47,26 +45,12 @@ pub fn snapshot_lua(
|
||||||
.items;
|
.items;
|
||||||
|
|
||||||
let (class_name, run_context) = match script_type {
|
let (class_name, run_context) = match script_type {
|
||||||
ScriptType::Server => {
|
ScriptType::Server => ("Script", run_context_enums.get("Server")),
|
||||||
if context.emit_legacy_scripts {
|
ScriptType::Client => ("Script", run_context_enums.get("Client")),
|
||||||
("Script", run_context_enums.get("Legacy"))
|
|
||||||
} else {
|
|
||||||
("Script", run_context_enums.get("Server"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ScriptType::Client => {
|
|
||||||
if context.emit_legacy_scripts {
|
|
||||||
("LocalScript", None)
|
|
||||||
} else {
|
|
||||||
("Script", run_context_enums.get("Client"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ScriptType::Module => ("ModuleScript", None),
|
ScriptType::Module => ("ModuleScript", None),
|
||||||
ScriptType::Plugin => ("Script", run_context_enums.get("Plugin")),
|
ScriptType::Plugin => ("Script", run_context_enums.get("Plugin")),
|
||||||
ScriptType::LegacyServer => ("Script", run_context_enums.get("Legacy")),
|
ScriptType::Legacy => ("Script", run_context_enums.get("Legacy")),
|
||||||
ScriptType::LegacyClient => ("LocalScript", None),
|
ScriptType::Local => ("LocalScript", None),
|
||||||
ScriptType::RunContextServer => ("Script", run_context_enums.get("Server")),
|
|
||||||
ScriptType::RunContextClient => ("Script", run_context_enums.get("Client")),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let contents = vfs.read_to_string_lf_normalized(path)?;
|
let contents = 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
|
/// Attempts to snapshot an 'init' Lua script contained inside of a folder with
|
||||||
/// the given name.
|
/// the given name.
|
||||||
///
|
///
|
||||||
/// Scripts named `init.lua`, `init.server.lua`, or `init.client.lua` usurp
|
/// Scripts named `init.lua`, `init.server.lua`, `init.client.lua`, `init.local.lua`,
|
||||||
/// their parents, which acts similarly to `__init__.py` from the Python world.
|
/// `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(
|
pub fn snapshot_lua_init(
|
||||||
context: &InstanceContext,
|
context: &InstanceContext,
|
||||||
vfs: &Vfs,
|
vfs: &Vfs,
|
||||||
|
|
@ -115,9 +100,8 @@ pub fn snapshot_lua_init(
|
||||||
|
|
||||||
if dir_snapshot.class_name != "Folder" {
|
if dir_snapshot.class_name != "Folder" {
|
||||||
anyhow::bail!(
|
anyhow::bail!(
|
||||||
"init.lua, init.server.lua, and init.client.lua can \
|
"init scripts can only be used if the instance produced by the \
|
||||||
only be used if the instance produced by the containing \
|
containing directory would be a Folder.\n\
|
||||||
directory would be a Folder.\n\
|
|
||||||
\n\
|
\n\
|
||||||
The directory {} turned into an instance of class {}.",
|
The directory {} turned into an instance of class {}.",
|
||||||
folder_path.display(),
|
folder_path.display(),
|
||||||
|
|
@ -177,6 +161,8 @@ pub fn syncback_lua<'sync>(
|
||||||
.strip_suffix(".server")
|
.strip_suffix(".server")
|
||||||
.or_else(|| file_stem.strip_suffix(".client"))
|
.or_else(|| file_stem.strip_suffix(".client"))
|
||||||
.or_else(|| file_stem.strip_suffix(".plugin"))
|
.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);
|
.unwrap_or(file_stem);
|
||||||
let meta_name = if meta_name.is_empty() {
|
let meta_name = if meta_name.is_empty() {
|
||||||
if snapshot.encode_windows_invalid_chars() {
|
if snapshot.encode_windows_invalid_chars() {
|
||||||
|
|
@ -211,7 +197,9 @@ pub fn syncback_lua_init<'sync>(
|
||||||
ScriptType::Server => "init.server.luau",
|
ScriptType::Server => "init.server.luau",
|
||||||
ScriptType::Client => "init.client.luau",
|
ScriptType::Client => "init.client.luau",
|
||||||
ScriptType::Module => "init.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")) {
|
let contents = if let Some(Variant::String(source)) = new_inst.properties.get(&ustr("Source")) {
|
||||||
|
|
@ -248,7 +236,7 @@ mod test {
|
||||||
use memofs::{InMemoryFs, VfsSnapshot};
|
use memofs::{InMemoryFs, VfsSnapshot};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn class_module_from_vfs() {
|
fn module_from_vfs() {
|
||||||
let mut imfs = InMemoryFs::new();
|
let mut imfs = InMemoryFs::new();
|
||||||
imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
|
imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -256,7 +244,7 @@ mod test {
|
||||||
let vfs = Vfs::new(imfs);
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua(
|
let instance_snapshot = snapshot_lua(
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
&InstanceContext::new(),
|
||||||
&vfs,
|
&vfs,
|
||||||
Path::new("/foo.lua"),
|
Path::new("/foo.lua"),
|
||||||
"foo",
|
"foo",
|
||||||
|
|
@ -271,30 +259,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runcontext_module_from_vfs() {
|
fn plugin_from_vfs() {
|
||||||
let mut imfs = InMemoryFs::new();
|
|
||||||
imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let vfs = Vfs::new(imfs);
|
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua(
|
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
|
||||||
&vfs,
|
|
||||||
Path::new("/foo.lua"),
|
|
||||||
"foo",
|
|
||||||
ScriptType::Module,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
insta::with_settings!({ sort_maps => true }, {
|
|
||||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn plugin_module_from_vfs() {
|
|
||||||
let mut imfs = InMemoryFs::new();
|
let mut imfs = InMemoryFs::new();
|
||||||
imfs.load_snapshot("/foo.plugin.lua", VfsSnapshot::file("Hello there!"))
|
imfs.load_snapshot("/foo.plugin.lua", VfsSnapshot::file("Hello there!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -302,7 +267,7 @@ mod test {
|
||||||
let vfs = Vfs::new(imfs);
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua(
|
let instance_snapshot = snapshot_lua(
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
&InstanceContext::new(),
|
||||||
&vfs,
|
&vfs,
|
||||||
Path::new("/foo.plugin.lua"),
|
Path::new("/foo.plugin.lua"),
|
||||||
"foo",
|
"foo",
|
||||||
|
|
@ -317,7 +282,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn class_server_from_vfs() {
|
fn server_from_vfs() {
|
||||||
let mut imfs = InMemoryFs::new();
|
let mut imfs = InMemoryFs::new();
|
||||||
imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
|
imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -325,7 +290,7 @@ mod test {
|
||||||
let vfs = Vfs::new(imfs);
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua(
|
let instance_snapshot = snapshot_lua(
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
&InstanceContext::new(),
|
||||||
&vfs,
|
&vfs,
|
||||||
Path::new("/foo.server.lua"),
|
Path::new("/foo.server.lua"),
|
||||||
"foo",
|
"foo",
|
||||||
|
|
@ -340,30 +305,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runcontext_server_from_vfs() {
|
fn client_from_vfs() {
|
||||||
let mut imfs = InMemoryFs::new();
|
|
||||||
imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let vfs = Vfs::new(imfs);
|
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua(
|
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
|
||||||
&vfs,
|
|
||||||
Path::new("/foo.server.lua"),
|
|
||||||
"foo",
|
|
||||||
ScriptType::Server,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
insta::with_settings!({ sort_maps => true }, {
|
|
||||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn class_client_from_vfs() {
|
|
||||||
let mut imfs = InMemoryFs::new();
|
let mut imfs = InMemoryFs::new();
|
||||||
imfs.load_snapshot("/foo.client.lua", VfsSnapshot::file("Hello there!"))
|
imfs.load_snapshot("/foo.client.lua", VfsSnapshot::file("Hello there!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -371,7 +313,7 @@ mod test {
|
||||||
let vfs = Vfs::new(imfs);
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua(
|
let instance_snapshot = snapshot_lua(
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
&InstanceContext::new(),
|
||||||
&vfs,
|
&vfs,
|
||||||
Path::new("/foo.client.lua"),
|
Path::new("/foo.client.lua"),
|
||||||
"foo",
|
"foo",
|
||||||
|
|
@ -386,19 +328,42 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runcontext_client_from_vfs() {
|
fn local_from_vfs() {
|
||||||
let mut imfs = InMemoryFs::new();
|
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();
|
.unwrap();
|
||||||
|
|
||||||
let vfs = Vfs::new(imfs);
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua(
|
let instance_snapshot = snapshot_lua(
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
&InstanceContext::new(),
|
||||||
&vfs,
|
&vfs,
|
||||||
Path::new("/foo.client.lua"),
|
Path::new("/foo.local.lua"),
|
||||||
"foo",
|
"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()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -420,7 +385,7 @@ mod test {
|
||||||
let vfs = Vfs::new(imfs);
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua_init(
|
let instance_snapshot = snapshot_lua_init(
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
&InstanceContext::new(),
|
||||||
&vfs,
|
&vfs,
|
||||||
Path::new("/root/init.lua"),
|
Path::new("/root/init.lua"),
|
||||||
"root",
|
"root",
|
||||||
|
|
@ -452,7 +417,7 @@ mod test {
|
||||||
let vfs = Vfs::new(imfs);
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua_init(
|
let instance_snapshot = snapshot_lua_init(
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
&InstanceContext::new(),
|
||||||
&vfs,
|
&vfs,
|
||||||
Path::new("/root/init.lua"),
|
Path::new("/root/init.lua"),
|
||||||
"root",
|
"root",
|
||||||
|
|
@ -467,7 +432,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn class_module_with_meta() {
|
fn module_with_meta() {
|
||||||
let mut imfs = InMemoryFs::new();
|
let mut imfs = InMemoryFs::new();
|
||||||
imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
|
imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -486,7 +451,7 @@ mod test {
|
||||||
let vfs = Vfs::new(imfs);
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua(
|
let instance_snapshot = snapshot_lua(
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
&InstanceContext::new(),
|
||||||
&vfs,
|
&vfs,
|
||||||
Path::new("/foo.lua"),
|
Path::new("/foo.lua"),
|
||||||
"foo",
|
"foo",
|
||||||
|
|
@ -501,41 +466,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runcontext_module_with_meta() {
|
fn server_with_meta() {
|
||||||
let mut imfs = InMemoryFs::new();
|
|
||||||
imfs.load_snapshot("/foo.lua", VfsSnapshot::file("Hello there!"))
|
|
||||||
.unwrap();
|
|
||||||
imfs.load_snapshot(
|
|
||||||
"/foo.meta.json",
|
|
||||||
VfsSnapshot::file(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"ignoreUnknownInstances": true
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let vfs = Vfs::new(imfs);
|
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua(
|
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
|
||||||
&vfs,
|
|
||||||
Path::new("/foo.lua"),
|
|
||||||
"foo",
|
|
||||||
ScriptType::Module,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
insta::with_settings!({ sort_maps => true }, {
|
|
||||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn class_script_with_meta() {
|
|
||||||
let mut imfs = InMemoryFs::new();
|
let mut imfs = InMemoryFs::new();
|
||||||
imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
|
imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -554,7 +485,7 @@ mod test {
|
||||||
let vfs = Vfs::new(imfs);
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua(
|
let instance_snapshot = snapshot_lua(
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
&InstanceContext::new(),
|
||||||
&vfs,
|
&vfs,
|
||||||
Path::new("/foo.server.lua"),
|
Path::new("/foo.server.lua"),
|
||||||
"foo",
|
"foo",
|
||||||
|
|
@ -569,41 +500,7 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runcontext_script_with_meta() {
|
fn server_disabled() {
|
||||||
let mut imfs = InMemoryFs::new();
|
|
||||||
imfs.load_snapshot("/foo.server.lua", VfsSnapshot::file("Hello there!"))
|
|
||||||
.unwrap();
|
|
||||||
imfs.load_snapshot(
|
|
||||||
"/foo.meta.json",
|
|
||||||
VfsSnapshot::file(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"ignoreUnknownInstances": true
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let vfs = Vfs::new(imfs);
|
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua(
|
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
|
||||||
&vfs,
|
|
||||||
Path::new("/foo.server.lua"),
|
|
||||||
"foo",
|
|
||||||
ScriptType::Server,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
insta::with_settings!({ sort_maps => true }, {
|
|
||||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn class_script_disabled() {
|
|
||||||
let mut imfs = InMemoryFs::new();
|
let mut imfs = InMemoryFs::new();
|
||||||
imfs.load_snapshot("/bar.server.lua", VfsSnapshot::file("Hello there!"))
|
imfs.load_snapshot("/bar.server.lua", VfsSnapshot::file("Hello there!"))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
@ -624,43 +521,7 @@ mod test {
|
||||||
let vfs = Vfs::new(imfs);
|
let vfs = Vfs::new(imfs);
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua(
|
let instance_snapshot = snapshot_lua(
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(true)),
|
&InstanceContext::new(),
|
||||||
&vfs,
|
|
||||||
Path::new("/bar.server.lua"),
|
|
||||||
"bar",
|
|
||||||
ScriptType::Server,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
insta::with_settings!({ sort_maps => true }, {
|
|
||||||
insta::assert_yaml_snapshot!(instance_snapshot);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn runcontext_script_disabled() {
|
|
||||||
let mut imfs = InMemoryFs::new();
|
|
||||||
imfs.load_snapshot("/bar.server.lua", VfsSnapshot::file("Hello there!"))
|
|
||||||
.unwrap();
|
|
||||||
imfs.load_snapshot(
|
|
||||||
"/bar.meta.json",
|
|
||||||
VfsSnapshot::file(
|
|
||||||
r#"
|
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"Disabled": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"#,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let vfs = Vfs::new(imfs);
|
|
||||||
|
|
||||||
let instance_snapshot = snapshot_lua(
|
|
||||||
&InstanceContext::with_emit_legacy_scripts(Some(false)),
|
|
||||||
&vfs,
|
&vfs,
|
||||||
Path::new("/bar.server.lua"),
|
Path::new("/bar.server.lua"),
|
||||||
"bar",
|
"bar",
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,14 @@ impl AdjacentMetadata {
|
||||||
// Use the file stem from the actual path (which has encoded chars)
|
// Use the file stem from the actual path (which has encoded chars)
|
||||||
// rather than the decoded name, so meta files are found correctly
|
// rather than the decoded name, so meta files are found correctly
|
||||||
// when encodeWindowsInvalidChars is enabled.
|
// when encodeWindowsInvalidChars is enabled.
|
||||||
// Strip known script type suffixes (.server, .client, .plugin) to get
|
// Strip known script type suffixes to get the base name for the meta file.
|
||||||
// the base name for the meta file.
|
|
||||||
let file_stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
let file_stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
|
||||||
let base_name = file_stem
|
let base_name = file_stem
|
||||||
.strip_suffix(".server")
|
.strip_suffix(".server")
|
||||||
.or_else(|| file_stem.strip_suffix(".client"))
|
.or_else(|| file_stem.strip_suffix(".client"))
|
||||||
.or_else(|| file_stem.strip_suffix(".plugin"))
|
.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);
|
.unwrap_or(file_stem);
|
||||||
let meta_path_json = path.with_file_name(format!("{base_name}.meta.json"));
|
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"));
|
let meta_path_jsonc = path.with_file_name(format!("{base_name}.meta.jsonc"));
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,7 @@ use self::{
|
||||||
yaml::snapshot_yaml,
|
yaml::snapshot_yaml,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{lua::ScriptType, project::snapshot_project_node, util::PathExt};
|
||||||
lua::ScriptType, project::snapshot_project_node, util::emit_legacy_scripts_default,
|
|
||||||
util::PathExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Returns an `InstanceSnapshot` for the provided path.
|
/// Returns an `InstanceSnapshot` for the provided path.
|
||||||
/// This will inspect the path and find the appropriate middleware for it,
|
/// 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?
|
// TODO: Is this even necessary anymore?
|
||||||
match file_name {
|
match file_name {
|
||||||
"init.server.luau" | "init.server.lua" | "init.client.luau" | "init.client.lua"
|
"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),
|
| "init.luau" | "init.lua" | "init.csv" => return Ok(None),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -131,6 +129,10 @@ fn get_dir_middleware<'path>(
|
||||||
(Middleware::ServerScriptDir, "init.server.lua"),
|
(Middleware::ServerScriptDir, "init.server.lua"),
|
||||||
(Middleware::ClientScriptDir, "init.client.luau"),
|
(Middleware::ClientScriptDir, "init.client.luau"),
|
||||||
(Middleware::ClientScriptDir, "init.client.lua"),
|
(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"),
|
(Middleware::CsvDir, "init.csv"),
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
@ -197,10 +199,8 @@ pub enum Middleware {
|
||||||
ClientScript,
|
ClientScript,
|
||||||
ModuleScript,
|
ModuleScript,
|
||||||
PluginScript,
|
PluginScript,
|
||||||
LegacyClientScript,
|
LocalScript,
|
||||||
LegacyServerScript,
|
LegacyScript,
|
||||||
RunContextServerScript,
|
|
||||||
RunContextClientScript,
|
|
||||||
Project,
|
Project,
|
||||||
Rbxm,
|
Rbxm,
|
||||||
Rbxmx,
|
Rbxmx,
|
||||||
|
|
@ -218,6 +218,10 @@ pub enum Middleware {
|
||||||
#[serde(skip_deserializing)]
|
#[serde(skip_deserializing)]
|
||||||
ModuleScriptDir,
|
ModuleScriptDir,
|
||||||
#[serde(skip_deserializing)]
|
#[serde(skip_deserializing)]
|
||||||
|
LocalScriptDir,
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
LegacyScriptDir,
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
CsvDir,
|
CsvDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,18 +243,8 @@ impl Middleware {
|
||||||
Self::ClientScript => snapshot_lua(context, vfs, path, name, ScriptType::Client),
|
Self::ClientScript => snapshot_lua(context, vfs, path, name, ScriptType::Client),
|
||||||
Self::ModuleScript => snapshot_lua(context, vfs, path, name, ScriptType::Module),
|
Self::ModuleScript => snapshot_lua(context, vfs, path, name, ScriptType::Module),
|
||||||
Self::PluginScript => snapshot_lua(context, vfs, path, name, ScriptType::Plugin),
|
Self::PluginScript => snapshot_lua(context, vfs, path, name, ScriptType::Plugin),
|
||||||
Self::LegacyClientScript => {
|
Self::LocalScript => snapshot_lua(context, vfs, path, name, ScriptType::Local),
|
||||||
snapshot_lua(context, vfs, path, name, ScriptType::LegacyClient)
|
Self::LegacyScript => snapshot_lua(context, vfs, path, name, ScriptType::Legacy),
|
||||||
}
|
|
||||||
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::Project => snapshot_project(context, vfs, path, name),
|
Self::Project => snapshot_project(context, vfs, path, name),
|
||||||
Self::Rbxm => snapshot_rbxm(context, vfs, path, name),
|
Self::Rbxm => snapshot_rbxm(context, vfs, path, name),
|
||||||
Self::Rbxmx => snapshot_rbxmx(context, vfs, path, name),
|
Self::Rbxmx => snapshot_rbxmx(context, vfs, path, name),
|
||||||
|
|
@ -269,6 +263,10 @@ impl Middleware {
|
||||||
Self::ModuleScriptDir => {
|
Self::ModuleScriptDir => {
|
||||||
snapshot_lua_init(context, vfs, path, name, ScriptType::Module)
|
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),
|
Self::CsvDir => snapshot_csv_init(context, vfs, path, name),
|
||||||
};
|
};
|
||||||
if let Ok(Some(ref mut snapshot)) = output {
|
if let Ok(Some(ref mut snapshot)) = output {
|
||||||
|
|
@ -299,6 +297,9 @@ impl Middleware {
|
||||||
Middleware::ServerScript => syncback_lua(snapshot),
|
Middleware::ServerScript => syncback_lua(snapshot),
|
||||||
Middleware::ClientScript => syncback_lua(snapshot),
|
Middleware::ClientScript => syncback_lua(snapshot),
|
||||||
Middleware::ModuleScript => 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::Rbxm => syncback_rbxm(snapshot),
|
||||||
Middleware::Rbxmx => syncback_rbxmx(snapshot),
|
Middleware::Rbxmx => syncback_rbxmx(snapshot),
|
||||||
Middleware::Toml => anyhow::bail!("cannot syncback Toml middleware"),
|
Middleware::Toml => anyhow::bail!("cannot syncback Toml middleware"),
|
||||||
|
|
@ -309,15 +310,9 @@ impl Middleware {
|
||||||
Middleware::ServerScriptDir => syncback_lua_init(ScriptType::Server, snapshot),
|
Middleware::ServerScriptDir => syncback_lua_init(ScriptType::Server, snapshot),
|
||||||
Middleware::ClientScriptDir => syncback_lua_init(ScriptType::Client, snapshot),
|
Middleware::ClientScriptDir => syncback_lua_init(ScriptType::Client, snapshot),
|
||||||
Middleware::ModuleScriptDir => syncback_lua_init(ScriptType::Module, 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::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::ServerScriptDir
|
||||||
| Middleware::ClientScriptDir
|
| Middleware::ClientScriptDir
|
||||||
| Middleware::ModuleScriptDir
|
| Middleware::ModuleScriptDir
|
||||||
|
| Middleware::LocalScriptDir
|
||||||
|
| Middleware::LegacyScriptDir
|
||||||
| Middleware::CsvDir
|
| Middleware::CsvDir
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -427,6 +424,10 @@ pub fn default_sync_rules() -> &'static [SyncRule] {
|
||||||
sync_rule!("*.client.luau", ClientScript, ".client.luau"),
|
sync_rule!("*.client.luau", ClientScript, ".client.luau"),
|
||||||
sync_rule!("*.plugin.lua", PluginScript, ".plugin.lua"),
|
sync_rule!("*.plugin.lua", PluginScript, ".plugin.lua"),
|
||||||
sync_rule!("*.plugin.luau", PluginScript, ".plugin.luau"),
|
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!("*.{lua,luau}", ModuleScript),
|
||||||
sync_rule!("*.project.json", Project, ".project.json"),
|
sync_rule!("*.project.json", Project, ".project.json"),
|
||||||
sync_rule!("*.project.jsonc", Project, ".project.jsonc"),
|
sync_rule!("*.project.jsonc", Project, ".project.jsonc"),
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ use crate::{
|
||||||
RojoRef,
|
RojoRef,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{emit_legacy_scripts_default, snapshot_from_vfs};
|
use super::snapshot_from_vfs;
|
||||||
|
|
||||||
pub fn snapshot_project(
|
pub fn snapshot_project(
|
||||||
context: &InstanceContext,
|
context: &InstanceContext,
|
||||||
|
|
@ -55,12 +55,6 @@ pub fn snapshot_project(
|
||||||
|
|
||||||
context.add_sync_rules(sync_rules);
|
context.add_sync_rules(sync_rules);
|
||||||
context.add_path_ignore_rules(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)? {
|
match snapshot_project_node(&context, path, project_name, &project.tree, vfs, None)? {
|
||||||
Some(found_snapshot) => {
|
Some(found_snapshot) => {
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@ metadata:
|
||||||
- /root/init.meta.jsonc
|
- /root/init.meta.jsonc
|
||||||
- /root/init.meta.json5
|
- /root/init.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@ metadata:
|
||||||
- /root/init.meta.jsonc
|
- /root/init.meta.jsonc
|
||||||
- /root/init.meta.json5
|
- /root/init.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: manually specified
|
specified_id: manually specified
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@ metadata:
|
||||||
- /foo/init.meta.jsonc
|
- /foo/init.meta.jsonc
|
||||||
- /foo/init.meta.json5
|
- /foo/init.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@ metadata:
|
||||||
- /foo/init.meta.jsonc
|
- /foo/init.meta.jsonc
|
||||||
- /foo/init.meta.json5
|
- /foo/init.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
@ -47,8 +46,7 @@ children:
|
||||||
- /foo/Child/init.meta.jsonc
|
- /foo/Child/init.meta.jsonc
|
||||||
- /foo/Child/init.meta.json5
|
- /foo/Child/init.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: dir
|
middleware: dir
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: manually specified
|
specified_id: manually specified
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ metadata:
|
||||||
relevant_paths:
|
relevant_paths:
|
||||||
- /foo.model.json
|
- /foo.model.json
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
@ -26,8 +25,7 @@ children:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ metadata:
|
||||||
relevant_paths:
|
relevant_paths:
|
||||||
- /foo.model.json
|
- /foo.model.json
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
@ -26,8 +25,7 @@ children:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -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: []
|
|
||||||
|
|
@ -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: []
|
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: false
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
@ -20,8 +20,7 @@ metadata:
|
||||||
- /root/init.meta.jsonc
|
- /root/init.meta.jsonc
|
||||||
- /root/init.meta.json5
|
- /root/init.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,7 @@ metadata:
|
||||||
- /root/init.meta.jsonc
|
- /root/init.meta.jsonc
|
||||||
- /root/init.meta.json5
|
- /root/init.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: manually specified
|
specified_id: manually specified
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,14 @@ snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
Path: /foo.server.lua
|
Path: /foo.legacy.lua
|
||||||
relevant_paths:
|
relevant_paths:
|
||||||
- /foo.server.lua
|
- /foo.legacy.lua
|
||||||
- /foo.meta.json
|
- /foo.meta.json
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
@ -6,15 +6,14 @@ snapshot_id: "00000000000000000000000000000000"
|
||||||
metadata:
|
metadata:
|
||||||
ignore_unknown_instances: false
|
ignore_unknown_instances: false
|
||||||
instigating_source:
|
instigating_source:
|
||||||
Path: /foo.client.lua
|
Path: /foo.local.lua
|
||||||
relevant_paths:
|
relevant_paths:
|
||||||
- /foo.client.lua
|
- /foo.local.lua
|
||||||
- /foo.meta.json
|
- /foo.meta.json
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
@ -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: []
|
||||||
|
|
@ -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: []
|
|
||||||
|
|
@ -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: []
|
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /bar.meta.jsonc
|
- /bar.meta.jsonc
|
||||||
- /bar.meta.json5
|
- /bar.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: false
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: false
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: false
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
@ -10,8 +10,7 @@ metadata:
|
||||||
- /foo/bar.meta.jsonc
|
- /foo/bar.meta.jsonc
|
||||||
- /foo/bar.meta.json5
|
- /foo/bar.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: manually specified
|
specified_id: manually specified
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ metadata:
|
||||||
- /foo/bar.meta.jsonc
|
- /foo/bar.meta.jsonc
|
||||||
- /foo/bar.meta.json5
|
- /foo/bar.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: manually specified
|
specified_id: manually specified
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ metadata:
|
||||||
- /foo/init.meta.jsonc
|
- /foo/init.meta.jsonc
|
||||||
- /foo/init.meta.json5
|
- /foo/init.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: manually specified
|
specified_id: manually specified
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ metadata:
|
||||||
- /foo/init.meta.jsonc
|
- /foo/init.meta.jsonc
|
||||||
- /foo/init.meta.json5
|
- /foo/init.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: manually specified
|
specified_id: manually specified
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ metadata:
|
||||||
relevant_paths:
|
relevant_paths:
|
||||||
- /foo/default.project.json
|
- /foo/default.project.json
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ metadata:
|
||||||
relevant_paths:
|
relevant_paths:
|
||||||
- /foo/hello.project.json
|
- /foo/hello.project.json
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ metadata:
|
||||||
- /foo/other.project.json
|
- /foo/other.project.json
|
||||||
- /foo/default.project.json
|
- /foo/default.project.json
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: project
|
middleware: project
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ metadata:
|
||||||
relevant_paths:
|
relevant_paths:
|
||||||
- /foo.project.json
|
- /foo.project.json
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
@ -31,8 +30,7 @@ children:
|
||||||
parent_class: Folder
|
parent_class: Folder
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ metadata:
|
||||||
- /foo/other.project.json
|
- /foo/other.project.json
|
||||||
- /foo/default.project.json
|
- /foo/default.project.json
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: project
|
middleware: project
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ metadata:
|
||||||
- /foo/other.project.json
|
- /foo/other.project.json
|
||||||
- /foo/default.project.json
|
- /foo/default.project.json
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: project
|
middleware: project
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
@ -32,8 +31,7 @@ children:
|
||||||
parent_class: Folder
|
parent_class: Folder
|
||||||
relevant_paths: []
|
relevant_paths: []
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,7 @@ metadata:
|
||||||
- /foo/other.meta.json5
|
- /foo/other.meta.json5
|
||||||
- /foo/default.project.json
|
- /foo/default.project.json
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: text
|
middleware: text
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ metadata:
|
||||||
relevant_paths:
|
relevant_paths:
|
||||||
- /foo.project.json
|
- /foo.project.json
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,7 @@ metadata:
|
||||||
relevant_paths:
|
relevant_paths:
|
||||||
- /foo.project.json
|
- /foo.project.json
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- foo.meta.jsonc
|
- foo.meta.jsonc
|
||||||
- foo.meta.json5
|
- foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: manually specified
|
specified_id: manually specified
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: manually specified
|
specified_id: manually specified
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- /foo.meta.jsonc
|
- /foo.meta.jsonc
|
||||||
- /foo.meta.json5
|
- /foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: ~
|
specified_id: ~
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,7 @@ metadata:
|
||||||
- foo.meta.jsonc
|
- foo.meta.jsonc
|
||||||
- foo.meta.json5
|
- foo.meta.json5
|
||||||
context:
|
context:
|
||||||
emit_legacy_scripts: true
|
decode_windows_invalid_chars: true
|
||||||
decode_windows_invalid_chars: false
|
|
||||||
specified_id: manually specified
|
specified_id: manually specified
|
||||||
middleware: ~
|
middleware: ~
|
||||||
schema: ~
|
schema: ~
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,3 @@ where
|
||||||
.with_context(|| format!("Path does not have a parent: {}", path.display()))
|
.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<bool> {
|
|
||||||
Some(true)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,8 @@ pub fn extension_for_middleware(middleware: Middleware) -> &'static str {
|
||||||
Middleware::ClientScript => "client.luau",
|
Middleware::ClientScript => "client.luau",
|
||||||
Middleware::ModuleScript => "luau",
|
Middleware::ModuleScript => "luau",
|
||||||
Middleware::PluginScript => "plugin.luau",
|
Middleware::PluginScript => "plugin.luau",
|
||||||
|
Middleware::LocalScript => "local.luau",
|
||||||
|
Middleware::LegacyScript => "legacy.luau",
|
||||||
Middleware::Project => "project.json5",
|
Middleware::Project => "project.json5",
|
||||||
Middleware::Rbxm => "rbxm",
|
Middleware::Rbxm => "rbxm",
|
||||||
Middleware::Rbxmx => "rbxmx",
|
Middleware::Rbxmx => "rbxmx",
|
||||||
|
|
@ -98,12 +100,6 @@ pub fn extension_for_middleware(middleware: Middleware) -> &'static str {
|
||||||
Middleware::Text => "txt",
|
Middleware::Text => "txt",
|
||||||
Middleware::Yaml => "yml",
|
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
|
// These are manually specified and not `_` to guard against future
|
||||||
// middleware additions missing this function.
|
// middleware additions missing this function.
|
||||||
Middleware::Ignore => unimplemented!("syncback does not work on Ignore middleware"),
|
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::CsvDir
|
||||||
| Middleware::ServerScriptDir
|
| Middleware::ServerScriptDir
|
||||||
| Middleware::ClientScriptDir
|
| Middleware::ClientScriptDir
|
||||||
| Middleware::ModuleScriptDir => {
|
| Middleware::ModuleScriptDir
|
||||||
|
| Middleware::LocalScriptDir
|
||||||
|
| Middleware::LegacyScriptDir => {
|
||||||
unimplemented!("directory middleware requires special treatment")
|
unimplemented!("directory middleware requires special treatment")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -325,8 +325,21 @@ pub fn get_best_middleware(snapshot: &SyncbackSnapshot) -> Middleware {
|
||||||
"Folder" | "Configuration" | "Tool" | "ScreenGui" | "SurfaceGui" | "BillboardGui"
|
"Folder" | "Configuration" | "Tool" | "ScreenGui" | "SurfaceGui" | "BillboardGui"
|
||||||
| "AdGui" => Middleware::Dir,
|
| "AdGui" => Middleware::Dir,
|
||||||
"StringValue" => Middleware::Text,
|
"StringValue" => Middleware::Text,
|
||||||
"Script" => Middleware::ServerScript,
|
"Script" => {
|
||||||
"LocalScript" => Middleware::ClientScript,
|
// 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,
|
"ModuleScript" => Middleware::ModuleScript,
|
||||||
"LocalizationTable" => Middleware::Csv,
|
"LocalizationTable" => Middleware::Csv,
|
||||||
// Default: use JsonModel for everything else (becomes Dir if has children)
|
// 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::ServerScript => Middleware::ServerScriptDir,
|
||||||
Middleware::ClientScript => Middleware::ClientScriptDir,
|
Middleware::ClientScript => Middleware::ClientScriptDir,
|
||||||
Middleware::ModuleScript => Middleware::ModuleScriptDir,
|
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::Csv => Middleware::CsvDir,
|
||||||
Middleware::JsonModel | Middleware::Text => Middleware::Dir,
|
Middleware::JsonModel | Middleware::Text => Middleware::Dir,
|
||||||
_ => middleware,
|
_ => middleware,
|
||||||
|
|
@ -431,11 +447,11 @@ impl SyncbackRules {
|
||||||
Ok(globs)
|
Ok(globs)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether Windows-invalid characters should be encoded in file
|
/// Returns whether special characters (including periods) should be encoded
|
||||||
/// names during syncback. Defaults to `false`.
|
/// in file names during syncback. Defaults to `true`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn encode_windows_invalid_chars(&self) -> bool {
|
pub fn encode_windows_invalid_chars(&self) -> bool {
|
||||||
self.encode_windows_invalid_chars.unwrap_or(false)
|
self.encode_windows_invalid_chars.unwrap_or(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
660
src/web/api.rs
660
src/web/api.rs
|
|
@ -257,6 +257,54 @@ impl ApiService {
|
||||||
/// Syncback an added instance by creating a file in the filesystem.
|
/// Syncback an added instance by creating a file in the filesystem.
|
||||||
/// The file watcher will pick up the change and update Rojo's tree.
|
/// 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
|
/// This follows the same middleware selection logic as the dedicated syncback
|
||||||
/// system in `src/syncback/mod.rs` to ensure consistent behavior.
|
/// system in `src/syncback/mod.rs` to ensure consistent behavior.
|
||||||
fn syncback_added_instance(
|
fn syncback_added_instance(
|
||||||
|
|
@ -280,6 +328,14 @@ impl ApiService {
|
||||||
.get_instance(added.parent)
|
.get_instance(added.parent)
|
||||||
.context("Parent instance not found in Rojo tree")?;
|
.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
|
// Get the parent's filesystem path from its metadata
|
||||||
let parent_path = parent_instance
|
let parent_path = parent_instance
|
||||||
.metadata()
|
.metadata()
|
||||||
|
|
@ -302,6 +358,54 @@ impl ApiService {
|
||||||
self.syncback_instance_to_path(added, &parent_dir)
|
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.
|
/// Recursively syncback an instance and its children to the filesystem.
|
||||||
/// This is the internal implementation that handles the actual file creation.
|
/// This is the internal implementation that handles the actual file creation.
|
||||||
fn syncback_instance_to_path(
|
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
|
// Determine the appropriate middleware/file format based on class name
|
||||||
// This matches the logic in src/syncback/mod.rs::get_best_middleware
|
// This matches the logic in src/syncback/mod.rs::get_best_middleware
|
||||||
|
|
@ -345,7 +451,7 @@ impl ApiService {
|
||||||
"Syncback: Created ModuleScript directory at {}",
|
"Syncback: Created ModuleScript directory at {}",
|
||||||
dir_path.display()
|
dir_path.display()
|
||||||
);
|
);
|
||||||
for child in &added.children {
|
for child in &unique_children {
|
||||||
self.syncback_instance_to_path(child, &dir_path)?;
|
self.syncback_instance_to_path(child, &dir_path)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -378,7 +484,7 @@ impl ApiService {
|
||||||
"Syncback: Created Script directory at {}",
|
"Syncback: Created Script directory at {}",
|
||||||
dir_path.display()
|
dir_path.display()
|
||||||
);
|
);
|
||||||
for child in &added.children {
|
for child in &unique_children {
|
||||||
self.syncback_instance_to_path(child, &dir_path)?;
|
self.syncback_instance_to_path(child, &dir_path)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -399,7 +505,7 @@ impl ApiService {
|
||||||
fs::create_dir_all(&dir_path).with_context(|| {
|
fs::create_dir_all(&dir_path).with_context(|| {
|
||||||
format!("Failed to create directory: {}", dir_path.display())
|
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(|| {
|
fs::write(&init_path, source.as_bytes()).with_context(|| {
|
||||||
format!("Failed to write file: {}", init_path.display())
|
format!("Failed to write file: {}", init_path.display())
|
||||||
})?;
|
})?;
|
||||||
|
|
@ -408,11 +514,11 @@ impl ApiService {
|
||||||
"Syncback: Created LocalScript directory at {}",
|
"Syncback: Created LocalScript directory at {}",
|
||||||
dir_path.display()
|
dir_path.display()
|
||||||
);
|
);
|
||||||
for child in &added.children {
|
for child in &unique_children {
|
||||||
self.syncback_instance_to_path(child, &dir_path)?;
|
self.syncback_instance_to_path(child, &dir_path)?;
|
||||||
}
|
}
|
||||||
} else {
|
} 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(|| {
|
fs::write(&file_path, source.as_bytes()).with_context(|| {
|
||||||
format!("Failed to write file: {}", file_path.display())
|
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)
|
// 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"")
|
fs::write(dir_path.join(".gitkeep"), b"")
|
||||||
.with_context(|| "Failed to write .gitkeep")?;
|
.with_context(|| "Failed to write .gitkeep")?;
|
||||||
}
|
}
|
||||||
|
|
@ -451,7 +558,7 @@ impl ApiService {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Recursively process children
|
// Recursively process children
|
||||||
for child in &added.children {
|
for child in &unique_children {
|
||||||
self.syncback_instance_to_path(child, &dir_path)?;
|
self.syncback_instance_to_path(child, &dir_path)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -469,7 +576,7 @@ impl ApiService {
|
||||||
"Syncback: Created StringValue directory at {}",
|
"Syncback: Created StringValue directory at {}",
|
||||||
dir_path.display()
|
dir_path.display()
|
||||||
);
|
);
|
||||||
for child in &added.children {
|
for child in &unique_children {
|
||||||
self.syncback_instance_to_path(child, &dir_path)?;
|
self.syncback_instance_to_path(child, &dir_path)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -508,7 +615,7 @@ impl ApiService {
|
||||||
"Syncback: Created LocalizationTable directory at {}",
|
"Syncback: Created LocalizationTable directory at {}",
|
||||||
dir_path.display()
|
dir_path.display()
|
||||||
);
|
);
|
||||||
for child in &added.children {
|
for child in &unique_children {
|
||||||
self.syncback_instance_to_path(child, &dir_path)?;
|
self.syncback_instance_to_path(child, &dir_path)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -542,7 +649,7 @@ impl ApiService {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Recursively process children
|
// Recursively process children
|
||||||
for child in &added.children {
|
for child in &unique_children {
|
||||||
self.syncback_instance_to_path(child, &dir_path)?;
|
self.syncback_instance_to_path(child, &dir_path)?;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -752,11 +859,9 @@ impl ApiService {
|
||||||
/// For `Script` class:
|
/// For `Script` class:
|
||||||
/// - RunContext: Client → "client"
|
/// - RunContext: Client → "client"
|
||||||
/// - RunContext: Server → "server"
|
/// - RunContext: Server → "server"
|
||||||
/// - RunContext: Legacy → "server" (default for legacy mode)
|
/// - RunContext: Legacy → "legacy"
|
||||||
/// - RunContext: Plugin → "plugin"
|
/// - RunContext: Plugin → "plugin"
|
||||||
/// - No RunContext → "server" (default)
|
/// - No RunContext → "legacy" (default)
|
||||||
///
|
|
||||||
/// This ensures proper round-tripping regardless of emitLegacyScripts mode.
|
|
||||||
fn get_script_suffix_for_run_context(
|
fn get_script_suffix_for_run_context(
|
||||||
&self,
|
&self,
|
||||||
added: &crate::web::interface::AddedInstance,
|
added: &crate::web::interface::AddedInstance,
|
||||||
|
|
@ -779,16 +884,16 @@ impl ApiService {
|
||||||
return match name.as_ref() {
|
return match name.as_ref() {
|
||||||
"Client" => "client",
|
"Client" => "client",
|
||||||
"Server" => "server",
|
"Server" => "server",
|
||||||
"Legacy" => "server",
|
"Legacy" => "legacy",
|
||||||
"Plugin" => "plugin",
|
"Plugin" => "plugin",
|
||||||
_ => "server",
|
_ => "legacy",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to server if no RunContext or unrecognized
|
// Default to legacy if no RunContext or unrecognized
|
||||||
"server"
|
"legacy"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write an adjacent meta file for scripts without children.
|
/// 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
|
// Test that script class names result in correct file extensions
|
||||||
let test_cases = vec![
|
let test_cases = vec![
|
||||||
("ModuleScript", "TestModule", "TestModule.luau"),
|
("ModuleScript", "TestModule", "TestModule.luau"),
|
||||||
("Script", "TestScript", "TestScript.server.luau"),
|
("LocalScript", "TestClient", "TestClient.local.luau"),
|
||||||
("LocalScript", "TestClient", "TestClient.client.luau"),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
for (class_name, name, expected) in test_cases {
|
for (class_name, name, expected) in test_cases {
|
||||||
let file_name = match class_name {
|
let file_name = match class_name {
|
||||||
"ModuleScript" => format!("{}.luau", name),
|
"ModuleScript" => format!("{}.luau", name),
|
||||||
"Script" => format!("{}.server.luau", name),
|
"LocalScript" => format!("{}.local.luau", name),
|
||||||
"LocalScript" => format!("{}.client.luau", name),
|
|
||||||
_ => format!("{}.model.json", name),
|
_ => format!("{}.model.json", name),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -1758,8 +1861,8 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_run_context_legacy_gives_server_suffix() {
|
fn test_run_context_legacy_gives_legacy_suffix() {
|
||||||
// Script with RunContext: Legacy should produce .server.luau (for legacy mode)
|
// Script with RunContext: Legacy should produce .legacy.luau
|
||||||
let run_context_enums = rbx_reflection_database::get()
|
let run_context_enums = rbx_reflection_database::get()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|db| db.enums.get("RunContext"))
|
.and_then(|db| db.enums.get("RunContext"))
|
||||||
|
|
@ -1792,14 +1895,13 @@ mod tests {
|
||||||
// For Script class, the file suffix is determined by RunContext:
|
// For Script class, the file suffix is determined by RunContext:
|
||||||
// - Client → .client.luau
|
// - Client → .client.luau
|
||||||
// - Server → .server.luau
|
// - Server → .server.luau
|
||||||
// - Legacy → .server.luau
|
// - Legacy → .legacy.luau
|
||||||
// - Plugin → .plugin.luau
|
// - Plugin → .plugin.luau
|
||||||
// This allows proper round-tripping regardless of emitLegacyScripts mode
|
|
||||||
|
|
||||||
let expected_mappings = vec![
|
let expected_mappings = vec![
|
||||||
("Client", "client"),
|
("Client", "client"),
|
||||||
("Server", "server"),
|
("Server", "server"),
|
||||||
("Legacy", "server"),
|
("Legacy", "legacy"),
|
||||||
("Plugin", "plugin"),
|
("Plugin", "plugin"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -1807,9 +1909,9 @@ mod tests {
|
||||||
let suffix = match run_context {
|
let suffix = match run_context {
|
||||||
"Client" => "client",
|
"Client" => "client",
|
||||||
"Server" => "server",
|
"Server" => "server",
|
||||||
"Legacy" => "server",
|
"Legacy" => "legacy",
|
||||||
"Plugin" => "plugin",
|
"Plugin" => "plugin",
|
||||||
_ => "server",
|
_ => "legacy",
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
suffix, expected_suffix,
|
suffix, expected_suffix,
|
||||||
|
|
@ -1820,14 +1922,14 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_local_script_always_client() {
|
fn test_local_script_always_local() {
|
||||||
// LocalScript class always produces .client.luau regardless of any properties
|
// LocalScript class always produces .local.luau regardless of any properties
|
||||||
let class_name = "LocalScript";
|
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" {
|
let suffix = if class_name == "LocalScript" {
|
||||||
"client"
|
"local"
|
||||||
} else {
|
} else {
|
||||||
"unknown"
|
"unknown"
|
||||||
};
|
};
|
||||||
|
|
@ -2136,8 +2238,6 @@ mod tests {
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
mod run_context_comprehensive_tests {
|
mod run_context_comprehensive_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use rbx_dom_weak::types::Enum;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Helper to get RunContext enum value by name
|
/// Helper to get RunContext enum value by name
|
||||||
fn get_run_context_value(name: &str) -> Option<u32> {
|
fn get_run_context_value(name: &str) -> Option<u32> {
|
||||||
|
|
@ -2817,7 +2917,7 @@ mod tests {
|
||||||
// In scripts-only mode, all instances get ignoreUnknownInstances: true
|
// In scripts-only mode, all instances get ignoreUnknownInstances: true
|
||||||
// This prevents accidental deletion of non-script instances
|
// This prevents accidental deletion of non-script instances
|
||||||
let folder = "Folder";
|
let folder = "Folder";
|
||||||
let is_script = is_script_class(folder);
|
let _is_script = is_script_class(folder);
|
||||||
|
|
||||||
// Non-scripts should have ignoreUnknownInstances set
|
// Non-scripts should have ignoreUnknownInstances set
|
||||||
let should_set_ignore = true; // All instances in scripts-only mode
|
let should_set_ignore = true; // All instances in scripts-only mode
|
||||||
|
|
@ -2868,18 +2968,18 @@ mod tests {
|
||||||
expected_extension: ".luau",
|
expected_extension: ".luau",
|
||||||
description: "ModuleScript without RunContext",
|
description: "ModuleScript without RunContext",
|
||||||
},
|
},
|
||||||
// LocalScript - always .client.luau
|
// LocalScript - always .local.luau
|
||||||
TestCase {
|
TestCase {
|
||||||
class_name: "LocalScript",
|
class_name: "LocalScript",
|
||||||
run_context: None,
|
run_context: None,
|
||||||
expected_extension: ".client.luau",
|
expected_extension: ".local.luau",
|
||||||
description: "LocalScript",
|
description: "LocalScript",
|
||||||
},
|
},
|
||||||
// Script with various RunContext values
|
// Script with various RunContext values
|
||||||
TestCase {
|
TestCase {
|
||||||
class_name: "Script",
|
class_name: "Script",
|
||||||
run_context: None,
|
run_context: None,
|
||||||
expected_extension: ".server.luau",
|
expected_extension: ".legacy.luau",
|
||||||
description: "Script without RunContext (default)",
|
description: "Script without RunContext (default)",
|
||||||
},
|
},
|
||||||
TestCase {
|
TestCase {
|
||||||
|
|
@ -2897,7 +2997,7 @@ mod tests {
|
||||||
TestCase {
|
TestCase {
|
||||||
class_name: "Script",
|
class_name: "Script",
|
||||||
run_context: Some("Legacy"),
|
run_context: Some("Legacy"),
|
||||||
expected_extension: ".server.luau",
|
expected_extension: ".legacy.luau",
|
||||||
description: "Script with RunContext::Legacy",
|
description: "Script with RunContext::Legacy",
|
||||||
},
|
},
|
||||||
TestCase {
|
TestCase {
|
||||||
|
|
@ -2911,16 +3011,16 @@ mod tests {
|
||||||
for case in test_cases {
|
for case in test_cases {
|
||||||
let suffix = match case.class_name {
|
let suffix = match case.class_name {
|
||||||
"ModuleScript" => String::new(),
|
"ModuleScript" => String::new(),
|
||||||
"LocalScript" => "client".to_string(),
|
"LocalScript" => "local".to_string(),
|
||||||
"Script" => {
|
"Script" => {
|
||||||
// Match on RunContext name to determine suffix
|
// Match on RunContext name to determine suffix
|
||||||
match case.run_context {
|
match case.run_context {
|
||||||
Some("Client") => "client",
|
Some("Client") => "client",
|
||||||
Some("Server") => "server",
|
Some("Server") => "server",
|
||||||
Some("Legacy") => "server",
|
Some("Legacy") => "legacy",
|
||||||
Some("Plugin") => "plugin",
|
Some("Plugin") => "plugin",
|
||||||
None => "server",
|
None => "legacy",
|
||||||
_ => "server",
|
_ => "legacy",
|
||||||
}
|
}
|
||||||
.to_string()
|
.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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ gen_build_tests! {
|
||||||
json_model_legacy_name,
|
json_model_legacy_name,
|
||||||
module_in_folder,
|
module_in_folder,
|
||||||
module_init,
|
module_init,
|
||||||
nested_runcontext,
|
|
||||||
optional,
|
optional,
|
||||||
project_composed_default,
|
project_composed_default,
|
||||||
project_composed_file,
|
project_composed_file,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue