diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__all_middleware.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__all_middleware.snap
index fc1be6fe3..5b10b4aec 100644
--- a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__all_middleware.snap
+++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__all_middleware.snap
@@ -4,7 +4,6 @@ assertion_line: 48
expression: "visualize_fs_snapshot(&fs_snapshot, &output_path)"
---
added_files:
- - default.project.json
- "src\\csv.csv"
- "src\\csv_init\\init.csv"
- "src\\dir\\client_script.client.luau"
diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__duplicate_rojo_id.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__duplicate_rojo_id.snap
new file mode 100644
index 000000000..407cd50fe
--- /dev/null
+++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__duplicate_rojo_id.snap
@@ -0,0 +1,11 @@
+---
+source: tests/rojo_test/syncback_util.rs
+assertion_line: 48
+expression: "visualize_fs_snapshot(&fs_snapshot, &output_path)"
+---
+added_files:
+ - container.model.json
+added_dirs: []
+removed_files: []
+removed_dirs: []
+
diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects.snap
index af2306edb..3531a587d 100644
--- a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects.snap
+++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects.snap
@@ -4,7 +4,6 @@ assertion_line: 48
expression: "visualize_fs_snapshot(&fs_snapshot, &output_path)"
---
added_files:
- - default.project.json
- nested.project.json
- string_value.txt
added_dirs: []
diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects_weird.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects_weird.snap
index 51d45a951..b9eac53e3 100644
--- a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects_weird.snap
+++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__nested_projects_weird.snap
@@ -4,9 +4,6 @@ assertion_line: 48
expression: "visualize_fs_snapshot(&fs_snapshot, &output_path)"
---
added_files:
- - client-only.project.json
- - default.project.json
- - server-only.project.json
- "src/modules\\ClientModule.luau"
- "src/modules\\ServerModule.luau"
added_dirs:
diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_all_middleware.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_all_middleware.snap
new file mode 100644
index 000000000..119ed2573
--- /dev/null
+++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_all_middleware.snap
@@ -0,0 +1,26 @@
+---
+source: tests/rojo_test/syncback_util.rs
+assertion_line: 48
+expression: "visualize_fs_snapshot(&fs_snapshot, &output_path)"
+---
+added_files:
+ - src/client_script.client.luau
+ - src/csv.csv
+ - "src/csv_init\\init.csv"
+ - "src/init_client_script\\init.client.lua"
+ - "src/init_module_script\\init.lua"
+ - "src/init_server_script\\init.server.lua"
+ - src/model_json.model.json
+ - src/module_script.luau
+ - src/project_json.project.json
+ - src/rbxmx.rbxmx
+ - src/server_script.server.luau
+ - src/text.txt
+added_dirs:
+ - src/csv_init
+ - src/init_client_script
+ - src/init_module_script
+ - src/init_server_script
+removed_files: []
+removed_dirs: []
+
diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_init.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_init.snap
index 1d17d5029..dc907e7a1 100644
--- a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_init.snap
+++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_init.snap
@@ -4,7 +4,6 @@ assertion_line: 48
expression: "visualize_fs_snapshot(&fs_snapshot, &output_path)"
---
added_files:
- - default.project.json
- "src\\init.lua"
added_dirs:
- src
diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_reserialize.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_reserialize.snap
new file mode 100644
index 000000000..e88330b24
--- /dev/null
+++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__project_reserialize.snap
@@ -0,0 +1,12 @@
+---
+source: tests/rojo_test/syncback_util.rs
+assertion_line: 48
+expression: "visualize_fs_snapshot(&fs_snapshot, &output_path)"
+---
+added_files:
+ - attribute_mismatch.luau
+ - property_mismatch.project.json
+added_dirs: []
+removed_files: []
+removed_dirs: []
+
diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback.snap
index ab189346c..7a29413b5 100644
--- a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback.snap
+++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__rbxm_fallback.snap
@@ -1,11 +1,11 @@
---
source: tests/rojo_test/syncback_util.rs
+assertion_line: 48
expression: "visualize_fs_snapshot(&fs_snapshot, &output_path)"
---
added_files:
- "ReplicatedStorage\\ChildWithDuplicates.rbxm"
- "ReplicatedStorage\\ChildWithoutDuplicates\\Child\\.gitkeep"
- - default.project.json
added_dirs:
- ReplicatedStorage
- "ReplicatedStorage\\ChildWithoutDuplicates"
diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties.snap
index a4a353073..69959cdc3 100644
--- a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties.snap
+++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties.snap
@@ -1,9 +1,9 @@
---
source: tests/rojo_test/syncback_util.rs
+assertion_line: 48
expression: "visualize_fs_snapshot(&fs_snapshot, &output_path)"
---
added_files:
- - default.project.json
- "src\\pointer.model.json"
- "src\\target.model.json"
added_dirs:
diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_update.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_update.snap
index b422e9982..69959cdc3 100644
--- a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_update.snap
+++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__ref_properties_update.snap
@@ -4,7 +4,6 @@ assertion_line: 48
expression: "visualize_fs_snapshot(&fs_snapshot, &output_path)"
---
added_files:
- - default.project.json
- "src\\pointer.model.json"
- "src\\target.model.json"
added_dirs:
diff --git a/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__string_value_project.snap b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__string_value_project.snap
new file mode 100644
index 000000000..fb423b5e7
--- /dev/null
+++ b/rojo-test/syncback-test-snapshots/end_to_end__rojo_test__syncback_util__string_value_project.snap
@@ -0,0 +1,12 @@
+---
+source: tests/rojo_test/syncback_util.rs
+assertion_line: 48
+expression: "visualize_fs_snapshot(&fs_snapshot, &output_path)"
+---
+added_files:
+ - default.project.json
+ - string_value.txt
+added_dirs: []
+removed_files: []
+removed_dirs: []
+
diff --git a/rojo-test/syncback-tests/duplicate_rojo_id/expected/container.model.json b/rojo-test/syncback-tests/duplicate_rojo_id/expected/container.model.json
new file mode 100644
index 000000000..aeb7b2b35
--- /dev/null
+++ b/rojo-test/syncback-tests/duplicate_rojo_id/expected/container.model.json
@@ -0,0 +1,21 @@
+{
+ "className": "Folder",
+ "children": [
+ {
+ "name": "value_1",
+ "className": "ObjectValue",
+ "attributes": {
+ "Rojo_Id": "value_1",
+ "Rojo_Target_Value": "value_1"
+ }
+ },
+ {
+ "name": "value_2",
+ "className": "ObjectValue",
+ "attributes": {
+ "Rojo_Id": "72bc28150ada2e6206442ee300004084",
+ "Rojo_Target_Value": "72bc28150ada2e6206442ee300004084"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/duplicate_rojo_id/expected/default.project.json b/rojo-test/syncback-tests/duplicate_rojo_id/expected/default.project.json
new file mode 100644
index 000000000..3343677aa
--- /dev/null
+++ b/rojo-test/syncback-tests/duplicate_rojo_id/expected/default.project.json
@@ -0,0 +1,11 @@
+{
+ "name": "duplicate_rojo_id",
+ "tree": {
+ "$className": "DataModel",
+ "ReplicatedStorage": {
+ "container": {
+ "$path": "container.model.json"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/duplicate_rojo_id/input.rbxl b/rojo-test/syncback-tests/duplicate_rojo_id/input.rbxl
new file mode 100644
index 000000000..8e3e83691
Binary files /dev/null and b/rojo-test/syncback-tests/duplicate_rojo_id/input.rbxl differ
diff --git a/rojo-test/syncback-tests/duplicate_rojo_id/output/container.model.json b/rojo-test/syncback-tests/duplicate_rojo_id/output/container.model.json
new file mode 100644
index 000000000..9c551865b
--- /dev/null
+++ b/rojo-test/syncback-tests/duplicate_rojo_id/output/container.model.json
@@ -0,0 +1,13 @@
+{
+ "className": "Folder",
+ "children": [
+ {
+ "name": "value_1",
+ "className": "ObjectValue"
+ },
+ {
+ "name": "value_2",
+ "className": "ObjectValue"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/duplicate_rojo_id/output/default.project.json b/rojo-test/syncback-tests/duplicate_rojo_id/output/default.project.json
new file mode 100644
index 000000000..3343677aa
--- /dev/null
+++ b/rojo-test/syncback-tests/duplicate_rojo_id/output/default.project.json
@@ -0,0 +1,11 @@
+{
+ "name": "duplicate_rojo_id",
+ "tree": {
+ "$className": "DataModel",
+ "ReplicatedStorage": {
+ "container": {
+ "$path": "container.model.json"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/default.project.json b/rojo-test/syncback-tests/project_all_middleware/expected/default.project.json
new file mode 100644
index 000000000..2fd8876cd
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/default.project.json
@@ -0,0 +1,53 @@
+{
+ "name": "project_all_middleware",
+ "tree": {
+ "$className": "DataModel",
+ "ReplicatedStorage": {
+ "client_script": {
+ "$path": "src/client_script.client.luau"
+ },
+ "csv": {
+ "$path": "src/csv.csv"
+ },
+ "csv_init": {
+ "$path": "src/csv_init"
+ },
+ "dir": {
+ "$path": "src/dir"
+ },
+ "dir_with_meta": {
+ "$path": "src/dir_with_meta"
+ },
+ "init_client_script": {
+ "$path": "src/init_client_script"
+ },
+ "init_module_script": {
+ "$path": "src/init_module_script"
+ },
+ "init_server_script": {
+ "$path": "src/init_server_script"
+ },
+ "model_json": {
+ "$path": "src/model_json.model.json"
+ },
+ "module_script": {
+ "$path": "src/module_script.luau"
+ },
+ "project_json": {
+ "$path": "src/project_json.project.json"
+ },
+ "rbxm": {
+ "$path": "src/rbxm.rbxm"
+ },
+ "rbxmx": {
+ "$path": "src/rbxmx.rbxmx"
+ },
+ "server_script": {
+ "$path": "src/server_script.server.luau"
+ },
+ "text": {
+ "$path": "src/text.txt"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/client_script.client.luau b/rojo-test/syncback-tests/project_all_middleware/expected/src/client_script.client.luau
new file mode 100644
index 000000000..1a6501120
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/src/client_script.client.luau
@@ -0,0 +1 @@
+-- ghostwriter notorious mutter restless punish
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/csv.csv b/rojo-test/syncback-tests/project_all_middleware/expected/src/csv.csv
new file mode 100644
index 000000000..b7c422222
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/src/csv.csv
@@ -0,0 +1,2 @@
+Key,Source,Context,Example,es
+Ack,Ack!,,An exclamation of despair,¡Ay!
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/csv_init/init.csv b/rojo-test/syncback-tests/project_all_middleware/expected/src/csv_init/init.csv
new file mode 100644
index 000000000..61a49274e
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/src/csv_init/init.csv
@@ -0,0 +1,2 @@
+Key,Source,Context,Example,en
+Rojo,Rojo,,Rojo is a really cool program,Red
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/dir_with_meta/init.meta.json b/rojo-test/syncback-tests/project_all_middleware/expected/src/dir_with_meta/init.meta.json
new file mode 100644
index 000000000..d62029576
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/src/dir_with_meta/init.meta.json
@@ -0,0 +1,3 @@
+{
+ "className": "Configuration"
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/init_client_script/init.client.lua b/rojo-test/syncback-tests/project_all_middleware/expected/src/init_client_script/init.client.lua
new file mode 100644
index 000000000..2330a0023
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/src/init_client_script/init.client.lua
@@ -0,0 +1 @@
+-- brag season coffin dilute flourish
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/init_module_script/init.lua b/rojo-test/syncback-tests/project_all_middleware/expected/src/init_module_script/init.lua
new file mode 100644
index 000000000..e36c2d878
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/src/init_module_script/init.lua
@@ -0,0 +1 @@
+-- absorb dragon coat crowd effect
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/init_server_script/init.server.lua b/rojo-test/syncback-tests/project_all_middleware/expected/src/init_server_script/init.server.lua
new file mode 100644
index 000000000..8518c74fe
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/src/init_server_script/init.server.lua
@@ -0,0 +1 @@
+-- rojo syncback very cool dekkonot
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/model_json.model.json b/rojo-test/syncback-tests/project_all_middleware/expected/src/model_json.model.json
new file mode 100644
index 000000000..a75990c38
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/src/model_json.model.json
@@ -0,0 +1,6 @@
+{
+ "className": "StringValue",
+ "properties": {
+ "Value": "i understand how person299 felt"
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/module_script.luau b/rojo-test/syncback-tests/project_all_middleware/expected/src/module_script.luau
new file mode 100644
index 000000000..b5c15e2b1
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/src/module_script.luau
@@ -0,0 +1 @@
+-- hospitality publish accumulation onion shaft
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/project_json.project.json b/rojo-test/syncback-tests/project_all_middleware/expected/src/project_json.project.json
new file mode 100644
index 000000000..cb0ce6a2e
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/src/project_json.project.json
@@ -0,0 +1,13 @@
+{
+ "name": "project_json",
+ "tree": {
+ "$className": "Color3Value",
+ "$properties": {
+ "Value": [
+ 1.0,
+ 0.5,
+ 0.0
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/rbxm.rbxm b/rojo-test/syncback-tests/project_all_middleware/expected/src/rbxm.rbxm
new file mode 100644
index 000000000..1fc6714ae
Binary files /dev/null and b/rojo-test/syncback-tests/project_all_middleware/expected/src/rbxm.rbxm differ
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/rbxmx.rbxmx b/rojo-test/syncback-tests/project_all_middleware/expected/src/rbxmx.rbxmx
new file mode 100644
index 000000000..50a48b184
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/src/rbxmx.rbxmx
@@ -0,0 +1,13 @@
+
+ -
+
+ rbxmx
+
+ 0
+ false
+ -1
+
+ ripe alike review heart dry
+
+
+
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/server_script.server.luau b/rojo-test/syncback-tests/project_all_middleware/expected/src/server_script.server.luau
new file mode 100644
index 000000000..ba51694c9
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/src/server_script.server.luau
@@ -0,0 +1 @@
+-- ostracize fraud consciousness seal architecture
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/expected/src/text.txt b/rojo-test/syncback-tests/project_all_middleware/expected/src/text.txt
new file mode 100644
index 000000000..a4cb228b7
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/expected/src/text.txt
@@ -0,0 +1 @@
+According to all known laws of aviation
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/input.rbxl b/rojo-test/syncback-tests/project_all_middleware/input.rbxl
new file mode 100644
index 000000000..824960bac
Binary files /dev/null and b/rojo-test/syncback-tests/project_all_middleware/input.rbxl differ
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/default.project.json b/rojo-test/syncback-tests/project_all_middleware/output/default.project.json
new file mode 100644
index 000000000..2fd8876cd
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/output/default.project.json
@@ -0,0 +1,53 @@
+{
+ "name": "project_all_middleware",
+ "tree": {
+ "$className": "DataModel",
+ "ReplicatedStorage": {
+ "client_script": {
+ "$path": "src/client_script.client.luau"
+ },
+ "csv": {
+ "$path": "src/csv.csv"
+ },
+ "csv_init": {
+ "$path": "src/csv_init"
+ },
+ "dir": {
+ "$path": "src/dir"
+ },
+ "dir_with_meta": {
+ "$path": "src/dir_with_meta"
+ },
+ "init_client_script": {
+ "$path": "src/init_client_script"
+ },
+ "init_module_script": {
+ "$path": "src/init_module_script"
+ },
+ "init_server_script": {
+ "$path": "src/init_server_script"
+ },
+ "model_json": {
+ "$path": "src/model_json.model.json"
+ },
+ "module_script": {
+ "$path": "src/module_script.luau"
+ },
+ "project_json": {
+ "$path": "src/project_json.project.json"
+ },
+ "rbxm": {
+ "$path": "src/rbxm.rbxm"
+ },
+ "rbxmx": {
+ "$path": "src/rbxmx.rbxmx"
+ },
+ "server_script": {
+ "$path": "src/server_script.server.luau"
+ },
+ "text": {
+ "$path": "src/text.txt"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/client_script.client.luau b/rojo-test/syncback-tests/project_all_middleware/output/src/client_script.client.luau
new file mode 100644
index 000000000..e69de29bb
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/csv.csv b/rojo-test/syncback-tests/project_all_middleware/output/src/csv.csv
new file mode 100644
index 000000000..c5f48af47
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/output/src/csv.csv
@@ -0,0 +1,2 @@
+Key,Source,Context,Example,es
+,,,,
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/csv_init/init.csv b/rojo-test/syncback-tests/project_all_middleware/output/src/csv_init/init.csv
new file mode 100644
index 000000000..24244d6c1
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/output/src/csv_init/init.csv
@@ -0,0 +1,2 @@
+Key,Source,Context,Example,en
+,,,,
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/dir_with_meta/init.meta.json b/rojo-test/syncback-tests/project_all_middleware/output/src/dir_with_meta/init.meta.json
new file mode 100644
index 000000000..d62029576
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/output/src/dir_with_meta/init.meta.json
@@ -0,0 +1,3 @@
+{
+ "className": "Configuration"
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/init_client_script/init.client.lua b/rojo-test/syncback-tests/project_all_middleware/output/src/init_client_script/init.client.lua
new file mode 100644
index 000000000..e69de29bb
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/init_module_script/init.lua b/rojo-test/syncback-tests/project_all_middleware/output/src/init_module_script/init.lua
new file mode 100644
index 000000000..e69de29bb
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/init_server_script/init.server.lua b/rojo-test/syncback-tests/project_all_middleware/output/src/init_server_script/init.server.lua
new file mode 100644
index 000000000..e69de29bb
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/model_json.model.json b/rojo-test/syncback-tests/project_all_middleware/output/src/model_json.model.json
new file mode 100644
index 000000000..a095016a7
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/output/src/model_json.model.json
@@ -0,0 +1,3 @@
+{
+ "className": "StringValue"
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/module_script.luau b/rojo-test/syncback-tests/project_all_middleware/output/src/module_script.luau
new file mode 100644
index 000000000..e69de29bb
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/project_json.project.json b/rojo-test/syncback-tests/project_all_middleware/output/src/project_json.project.json
new file mode 100644
index 000000000..ed8452050
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/output/src/project_json.project.json
@@ -0,0 +1,6 @@
+{
+ "name": "project_json",
+ "tree": {
+ "$className": "Color3Value"
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/rbxm.rbxm b/rojo-test/syncback-tests/project_all_middleware/output/src/rbxm.rbxm
new file mode 100644
index 000000000..1fc6714ae
Binary files /dev/null and b/rojo-test/syncback-tests/project_all_middleware/output/src/rbxm.rbxm differ
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/rbxmx.rbxmx b/rojo-test/syncback-tests/project_all_middleware/output/src/rbxmx.rbxmx
new file mode 100644
index 000000000..81a52114b
--- /dev/null
+++ b/rojo-test/syncback-tests/project_all_middleware/output/src/rbxmx.rbxmx
@@ -0,0 +1,7 @@
+
+ -
+
+ rbxmx
+
+
+
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/server_script.server.luau b/rojo-test/syncback-tests/project_all_middleware/output/src/server_script.server.luau
new file mode 100644
index 000000000..e69de29bb
diff --git a/rojo-test/syncback-tests/project_all_middleware/output/src/text.txt b/rojo-test/syncback-tests/project_all_middleware/output/src/text.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/rojo-test/syncback-tests/project_reserialize/expected/attribute_mismatch.luau b/rojo-test/syncback-tests/project_reserialize/expected/attribute_mismatch.luau
new file mode 100644
index 000000000..b6c5da36f
--- /dev/null
+++ b/rojo-test/syncback-tests/project_reserialize/expected/attribute_mismatch.luau
@@ -0,0 +1 @@
+-- satellite beef psychology response supply
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_reserialize/expected/default.project.json b/rojo-test/syncback-tests/project_reserialize/expected/default.project.json
new file mode 100644
index 000000000..bf56d2e07
--- /dev/null
+++ b/rojo-test/syncback-tests/project_reserialize/expected/default.project.json
@@ -0,0 +1,29 @@
+{
+ "name": "project_reserialize",
+ "tree": {
+ "$className": "DataModel",
+ "Workspace": {
+ "attribute_mismatch": {
+ "$attributes": {
+ "foo": "bar"
+ },
+ "$path": "attribute_mismatch.luau"
+ },
+ "property_mismatch": {
+ "$path": "property_mismatch.project.json"
+ },
+ "$properties": {
+ "CSGAsyncDynamicCollision": {
+ "Enum": 0
+ },
+ "DecreaseMinimumPartDensityMode": {
+ "Enum": 0
+ },
+ "StreamingEnabled": true
+ },
+ "$attributes": {
+ "Rojo_Target_CurrentCamera": "6d6ae1d713c82fae0620aa1300000375"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_reserialize/expected/property_mismatch.project.json b/rojo-test/syncback-tests/project_reserialize/expected/property_mismatch.project.json
new file mode 100644
index 000000000..1ca8b303e
--- /dev/null
+++ b/rojo-test/syncback-tests/project_reserialize/expected/property_mismatch.project.json
@@ -0,0 +1,11 @@
+{
+ "name": "property_mismatch",
+ "tree": {
+ "$className": "BrickColorValue",
+ "$properties": {
+ "Value": {
+ "BrickColor": 345
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_reserialize/input.rbxl b/rojo-test/syncback-tests/project_reserialize/input.rbxl
new file mode 100644
index 000000000..6d5da6818
Binary files /dev/null and b/rojo-test/syncback-tests/project_reserialize/input.rbxl differ
diff --git a/rojo-test/syncback-tests/project_reserialize/output/attribute_mismatch.luau b/rojo-test/syncback-tests/project_reserialize/output/attribute_mismatch.luau
new file mode 100644
index 000000000..e69de29bb
diff --git a/rojo-test/syncback-tests/project_reserialize/output/default.project.json b/rojo-test/syncback-tests/project_reserialize/output/default.project.json
new file mode 100644
index 000000000..bf56d2e07
--- /dev/null
+++ b/rojo-test/syncback-tests/project_reserialize/output/default.project.json
@@ -0,0 +1,29 @@
+{
+ "name": "project_reserialize",
+ "tree": {
+ "$className": "DataModel",
+ "Workspace": {
+ "attribute_mismatch": {
+ "$attributes": {
+ "foo": "bar"
+ },
+ "$path": "attribute_mismatch.luau"
+ },
+ "property_mismatch": {
+ "$path": "property_mismatch.project.json"
+ },
+ "$properties": {
+ "CSGAsyncDynamicCollision": {
+ "Enum": 0
+ },
+ "DecreaseMinimumPartDensityMode": {
+ "Enum": 0
+ },
+ "StreamingEnabled": true
+ },
+ "$attributes": {
+ "Rojo_Target_CurrentCamera": "6d6ae1d713c82fae0620aa1300000375"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/project_reserialize/output/property_mismatch.project.json b/rojo-test/syncback-tests/project_reserialize/output/property_mismatch.project.json
new file mode 100644
index 000000000..cb34ee96d
--- /dev/null
+++ b/rojo-test/syncback-tests/project_reserialize/output/property_mismatch.project.json
@@ -0,0 +1,6 @@
+{
+ "name": "property_mismatch",
+ "tree": {
+ "$className": "BrickColorValue"
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/string_value_project/expected/default.project.json b/rojo-test/syncback-tests/string_value_project/expected/default.project.json
new file mode 100644
index 000000000..7e717416c
--- /dev/null
+++ b/rojo-test/syncback-tests/string_value_project/expected/default.project.json
@@ -0,0 +1,20 @@
+{
+ "name": "string_value_project",
+ "tree": {
+ "$className": "DataModel",
+ "ReplicatedStorage": {
+ "inside_project_file": {
+ "$className": "StringValue",
+ "$properties": {
+ "Value": "imgettingverytiredofwritingthesetests2"
+ }
+ },
+ "on_file_system": {
+ "$attributes": {
+ "imgettingverytiredofwritingthesetests": "person299 was ahead of his time"
+ },
+ "$path": "string_value.txt"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/string_value_project/expected/string_value.txt b/rojo-test/syncback-tests/string_value_project/expected/string_value.txt
new file mode 100644
index 000000000..a29b38294
--- /dev/null
+++ b/rojo-test/syncback-tests/string_value_project/expected/string_value.txt
@@ -0,0 +1 @@
+shout out to the brown bug anthology in person299's admin commands
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/string_value_project/input.rbxl b/rojo-test/syncback-tests/string_value_project/input.rbxl
new file mode 100644
index 000000000..cea6a4853
Binary files /dev/null and b/rojo-test/syncback-tests/string_value_project/input.rbxl differ
diff --git a/rojo-test/syncback-tests/string_value_project/output/default.project.json b/rojo-test/syncback-tests/string_value_project/output/default.project.json
new file mode 100644
index 000000000..00fa7c2d2
--- /dev/null
+++ b/rojo-test/syncback-tests/string_value_project/output/default.project.json
@@ -0,0 +1,14 @@
+{
+ "name": "string_value_project",
+ "tree": {
+ "$className": "DataModel",
+ "ReplicatedStorage": {
+ "on_file_system": {
+ "$path": "string_value.txt"
+ },
+ "inside_project_file": {
+ "$className": "StringValue"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/syncback-tests/string_value_project/output/string_value.txt b/rojo-test/syncback-tests/string_value_project/output/string_value.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/cli/syncback.rs b/src/cli/syncback.rs
index d8782fac7..4ce557ffd 100644
--- a/src/cli/syncback.rs
+++ b/src/cli/syncback.rs
@@ -254,11 +254,14 @@ fn list_files(snapshot: &FsSnapshot, color: ColorChoice, base_path: &Path) -> io
let mut buffer = writer.buffer();
if snapshot.is_empty() {
- writeln!(&mut buffer, "No files/added would be removed or added.")?;
+ writeln!(
+ &mut buffer,
+ "No files/directories would be removed or added."
+ )?;
} else {
let added = snapshot.added_paths();
if !added.is_empty() {
- writeln!(&mut buffer, "Writing files/folders:")?;
+ writeln!(&mut buffer, "Writing files/directories:")?;
buffer.set_color(&add_color)?;
for path in added {
writeln!(
@@ -271,7 +274,7 @@ fn list_files(snapshot: &FsSnapshot, color: ColorChoice, base_path: &Path) -> io
}
let removed = snapshot.removed_paths();
if !removed.is_empty() {
- writeln!(&mut buffer, "Removing files/folders:")?;
+ writeln!(&mut buffer, "Removing files/directories:")?;
buffer.set_color(&remove_color)?;
for path in removed {
writeln!(
diff --git a/src/snapshot/metadata.rs b/src/snapshot/metadata.rs
index 55158518a..ffdaedbe4 100644
--- a/src/snapshot/metadata.rs
+++ b/src/snapshot/metadata.rs
@@ -63,6 +63,8 @@ pub struct InstanceMetadata {
/// Indicates the ID used for Ref properties pointing to this Instance.
pub specified_id: Option,
+ /// The Middleware that was used to create this Instance. Should generally
+ /// not be `None` except if the snapshotting process is not completed.
pub middleware: Option,
}
@@ -106,6 +108,13 @@ impl InstanceMetadata {
}
}
+ pub fn middleware(self, middleware: Middleware) -> Self {
+ Self {
+ middleware: Some(middleware),
+ ..self
+ }
+ }
+
pub fn specified_id(self, id: Option) -> Self {
Self {
specified_id: id,
diff --git a/src/snapshot_middleware/meta_file.rs b/src/snapshot_middleware/meta_file.rs
index 089fffa60..604de4500 100644
--- a/src/snapshot_middleware/meta_file.rs
+++ b/src/snapshot_middleware/meta_file.rs
@@ -81,7 +81,7 @@ impl AdjacentMetadata {
.unwrap_or_default();
let class = &snapshot.new_inst().class;
- for (name, value) in snapshot.get_filtered_properties(snapshot.new).unwrap() {
+ for (name, value) in snapshot.get_path_filtered_properties(snapshot.new).unwrap() {
match value {
Variant::Attributes(attrs) => {
for (attr_name, attr_value) in attrs.iter() {
@@ -262,7 +262,7 @@ impl DirectoryMetadata {
.unwrap_or_default();
let class = &snapshot.new_inst().class;
- for (name, value) in snapshot.get_filtered_properties(snapshot.new).unwrap() {
+ for (name, value) in snapshot.get_path_filtered_properties(snapshot.new).unwrap() {
match value {
Variant::Attributes(attrs) => {
for (name, value) in attrs.iter() {
diff --git a/src/snapshot_middleware/mod.rs b/src/snapshot_middleware/mod.rs
index cd0ccb39f..029f4f305 100644
--- a/src/snapshot_middleware/mod.rs
+++ b/src/snapshot_middleware/mod.rs
@@ -284,6 +284,21 @@ impl Middleware {
)
}
+ /// Returns whether this particular middleware sets its own properties.
+ /// This applies to things like `JsonModel` and `Project`, since they
+ /// set properties without needing a meta.json file.
+ ///
+ /// It does not cover middleware like `ServerScript` or `Csv` because they
+ /// need a meta.json file set properties that aren't their designated
+ /// 'special' properties.
+ #[inline]
+ pub fn handles_own_properties(&self) -> bool {
+ matches!(
+ self,
+ Middleware::JsonModel | Middleware::Project | Middleware::Rbxm | Middleware::Rbxmx
+ )
+ }
+
/// Attempts to return a middleware that should be used for the given path.
///
/// Returns `Err` only if the Vfs cannot read information about the path.
diff --git a/src/snapshot_middleware/project.rs b/src/snapshot_middleware/project.rs
index 4009a0289..01e33282f 100644
--- a/src/snapshot_middleware/project.rs
+++ b/src/snapshot_middleware/project.rs
@@ -20,7 +20,8 @@ use crate::{
PathIgnoreRule, SyncRule,
},
snapshot_middleware::Middleware,
- syncback::{FsSnapshot, SyncbackReturn, SyncbackSnapshot},
+ syncback::{filter_properties, FsSnapshot, SyncbackReturn, SyncbackSnapshot},
+ variant_eq::variant_eq,
RojoRef,
};
@@ -341,6 +342,7 @@ pub fn syncback_project<'sync>(
let mut old_child_map = HashMap::new();
let mut new_child_map = HashMap::new();
+ let mut node_changed_map = Vec::new();
let mut node_queue = VecDeque::with_capacity(1);
node_queue.push_back((&mut project.tree, old_inst, snapshot.new_inst()));
@@ -348,51 +350,21 @@ pub fn syncback_project<'sync>(
log::debug!("Processing node {}", old_inst.name());
if old_inst.class_name() != new_inst.class {
anyhow::bail!(
- "Cannot change the class of {} in project file {}",
+ "Cannot change the class of {} in project file {}.\n\
+ Current class is {}, it is a {} in the input file.",
old_inst.name(),
- project_path.display()
+ project_path.display(),
+ old_inst.class_name(),
+ new_inst.class
);
}
- let filtered_properties = snapshot
- .get_filtered_properties(new_inst.referent())
- .unwrap();
- let properties = &mut node.properties;
- let mut attributes = BTreeMap::new();
-
- // We would ideally have different behavior here based on whether a
- // node has `$path` set. However, due to an issue with Middleware not
- // knowing whether they originate from a project or not, we just skip
- // writing metadata for things from projects. So to avoid properties
- // being dropped, we don't filter them specially.
- // TODO: We should handle this branching somehow so that meta.json files
- // are respected.
- for (name, value) in filtered_properties {
- match value {
- Variant::Attributes(attrs) => {
- for (attr_name, attr_value) in attrs.iter() {
- attributes.insert(
- attr_name.clone(),
- UnresolvedValue::from_variant_unambiguous(attr_value.clone()),
- );
- }
- }
- Variant::SharedString(_) => {
- log::warn!(
- "Rojo cannot serialize the property {}.{name} in project files.\n\
- If this is not acceptable, resave the Instance at '{}' manually as an RBXM or RBXMX.", new_inst.class, snapshot.get_new_inst_path(snapshot.new)
- );
- }
- _ => {
- properties.insert(
- name.to_string(),
- UnresolvedValue::from_variant(value.clone(), &new_inst.class, name),
- );
- }
- }
- }
- node.attributes = attributes;
-
+ // TODO handle meta.json files in this branch. Right now, we perform
+ // syncback if a node has `$path` set but the Middleware aren't aware
+ // that the Instances they're running on originate in a project.json.
+ // As a result, the `meta.json` syncback code is hardcoded to not work
+ // if the Instance originates from a project file. However, we should
+ // ideally use a .meta.json over the project node if it exists already.
if node.path.is_some() {
// Since the node has a path, we have to run syncback on it.
let node_path = node.path.as_ref().map(PathNode::path).expect(
@@ -405,16 +377,34 @@ pub fn syncback_project<'sync>(
base_path.join(node_path)
};
- let snapshot = syncback_project_node(
- snapshot,
+ let middleware = match Middleware::middleware_for_path(
+ snapshot.vfs(),
&project.sync_rules,
&full_path,
- new_inst,
- old_inst,
- )?;
- descendant_snapshots.push(snapshot);
+ )? {
+ Some(middleware) => middleware,
+ // The only way this can happen at this point is if the path does
+ // not exist on the file system or there's no middleware for it.
+ None => anyhow::bail!(
+ "path does not exist or could not be turned into a file Rojo understands: {}",
+ full_path.display()
+ ),
+ };
+
+ descendant_snapshots.push(
+ snapshot
+ .with_new_path(full_path.clone(), new_inst.referent(), Some(old_inst.id()))
+ .middleware(middleware),
+ );
ref_to_path_map.insert(new_inst.referent(), full_path);
+
+ // We only want to set properties if it needs it.
+ if !middleware.handles_own_properties() {
+ project_node_property_syncback_path(snapshot, new_inst, node);
+ }
+ } else {
+ project_node_property_syncback_no_path(snapshot, new_inst, node);
}
for child_ref in new_inst.children() {
@@ -511,41 +501,126 @@ pub fn syncback_project<'sync>(
}
}
removed_descendants.extend(old_child_map.drain().map(|(_, v)| v));
+ node_changed_map.push((&node.properties, &node.attributes, old_inst))
+ }
+ let mut fs_snapshot = FsSnapshot::new();
+
+ for (node_properties, node_attributes, old_inst) in node_changed_map {
+ if project_node_should_reserialize(node_properties, node_attributes, old_inst)? {
+ fs_snapshot.add_file(project_path, serde_json::to_vec_pretty(&project)?);
+ break;
+ }
}
Ok(SyncbackReturn {
inst_snapshot: InstanceSnapshot::from_instance(snapshot.new_inst()),
- fs_snapshot: FsSnapshot::new()
- .with_added_file(project_path, serde_json::to_vec_pretty(&project)?),
+ fs_snapshot,
children: descendant_snapshots,
removed_children: removed_descendants,
})
}
-fn syncback_project_node<'sync>(
- snapshot: &SyncbackSnapshot<'sync>,
- sync_rules: &[SyncRule],
- node_path: &Path,
+fn project_node_property_syncback<'inst>(
+ snapshot: &SyncbackSnapshot,
+ filtered_properties: HashMap<&'inst str, &'inst Variant>,
new_inst: &Instance,
- old_inst: InstanceWithMeta,
-) -> anyhow::Result> {
- let middleware = match Middleware::middleware_for_path(snapshot.vfs(), sync_rules, node_path)? {
- Some(stuff) => stuff,
- // The only way this can happen at this point is if the path does
- // not exist on the file system or there's no middleware for it.
- None => anyhow::bail!(
- "path does not exist or could not be turned into a file Rojo understands: {}",
- node_path.display()
- ),
- };
+ node: &mut ProjectNode,
+) {
+ let properties = &mut node.properties;
+ let mut attributes = BTreeMap::new();
+ for (name, value) in filtered_properties {
+ match value {
+ Variant::Attributes(attrs) => {
+ for (attr_name, attr_value) in attrs.iter() {
+ attributes.insert(
+ attr_name.clone(),
+ UnresolvedValue::from_variant_unambiguous(attr_value.clone()),
+ );
+ }
+ }
+ Variant::SharedString(_) => {
+ log::warn!(
+ "Rojo cannot serialize the property {}.{name} in project files.\n\
+ If this is not acceptable, resave the Instance at '{}' manually as an RBXM or RBXMX.", new_inst.class, snapshot.get_new_inst_path(snapshot.new)
+ );
+ }
+ _ => {
+ properties.insert(
+ name.to_string(),
+ UnresolvedValue::from_variant(value.clone(), &new_inst.class, name),
+ );
+ }
+ }
+ }
+ node.attributes = attributes;
+}
- Ok(snapshot
- .with_new_path(
- node_path.to_path_buf(),
- new_inst.referent(),
- Some(old_inst.id()),
- )
- .middleware(middleware))
+fn project_node_property_syncback_path(
+ snapshot: &SyncbackSnapshot,
+ new_inst: &Instance,
+ node: &mut ProjectNode,
+) {
+ let filtered_properties = snapshot
+ .get_path_filtered_properties(new_inst.referent())
+ .unwrap();
+ project_node_property_syncback(snapshot, filtered_properties, new_inst, node)
+}
+
+fn project_node_property_syncback_no_path(
+ snapshot: &SyncbackSnapshot,
+ new_inst: &Instance,
+ node: &mut ProjectNode,
+) {
+ let filtered_properties = filter_properties(snapshot.project(), new_inst);
+ project_node_property_syncback(snapshot, filtered_properties, new_inst, node)
+}
+
+fn project_node_should_reserialize(
+ node_properties: &BTreeMap,
+ node_attributes: &BTreeMap,
+ instance: InstanceWithMeta,
+) -> anyhow::Result {
+ for (prop_name, unresolved_node_value) in node_properties {
+ if let Some(inst_value) = instance.properties().get(prop_name) {
+ let node_value = unresolved_node_value
+ .clone()
+ .resolve(instance.name(), prop_name)?;
+ if !variant_eq(inst_value, &node_value) {
+ return Ok(true);
+ }
+ } else {
+ return Ok(true);
+ }
+ }
+
+ match instance.properties().get("Attributes") {
+ Some(Variant::Attributes(inst_attributes)) => {
+ // This will also catch if one is empty but the other isn't
+ if node_attributes.len() != inst_attributes.len() {
+ Ok(true)
+ } else {
+ for (attr_name, unresolved_node_value) in node_attributes {
+ if let Some(inst_value) = inst_attributes.get(attr_name.as_str()) {
+ let node_value = unresolved_node_value.clone().resolve_unambiguous()?;
+ if !variant_eq(inst_value, &node_value) {
+ return Ok(true);
+ }
+ } else {
+ return Ok(true);
+ }
+ }
+ Ok(false)
+ }
+ }
+ Some(_) => Ok(true),
+ None => {
+ if !node_attributes.is_empty() {
+ Ok(true)
+ } else {
+ Ok(false)
+ }
+ }
+ }
}
fn infer_class_name(name: &str, parent_class: Option<&str>) -> Option> {
diff --git a/src/syncback/mod.rs b/src/syncback/mod.rs
index 7242ba90e..70ae56167 100644
--- a/src/syncback/mod.rs
+++ b/src/syncback/mod.rs
@@ -63,7 +63,7 @@ pub fn syncback_loop(
strip_unknown_root_children(&mut new_tree, old_tree);
log::debug!("Collecting referents for new DOM...");
- let deferred_referents = collect_referents(&new_tree)?;
+ let deferred_referents = collect_referents(&new_tree);
log::debug!("Pre-filtering properties on DOMs");
for referent in descendants(&new_tree, new_tree.root_ref()) {
@@ -113,6 +113,8 @@ pub fn syncback_loop(
log::debug!("Skipping referent linking as per project syncback rules");
}
+ new_tree.root_mut().name = project.name.clone();
+
log::debug!("Hashing project DOM");
let old_hashes = hash_tree(project, old_tree.inner(), old_tree.get_root_id());
log::debug!("Hashing file DOM");
@@ -265,7 +267,7 @@ pub fn get_best_middleware(snapshot: &SyncbackSnapshot) -> Middleware {
let mut middleware;
if let Some(override_middleware) = snapshot.middleware {
- middleware = override_middleware;
+ return override_middleware;
} else if let Some(old_middleware) = old_middleware {
return old_middleware;
} else if json_model_classes.contains(inst.class.as_str()) {
diff --git a/src/syncback/ref_properties.rs b/src/syncback/ref_properties.rs
index cc08267c3..26544d9b8 100644
--- a/src/syncback/ref_properties.rs
+++ b/src/syncback/ref_properties.rs
@@ -1,17 +1,25 @@
//! Implements iterating through an entire WeakDom and linking all Ref
//! properties using attributes.
-use std::collections::{HashMap, VecDeque};
+use std::collections::{hash_map::Entry, HashMap, VecDeque};
use rbx_dom_weak::{
- types::{Attributes, Ref, Variant},
+ types::{Attributes, Ref, UniqueId, Variant},
Instance, WeakDom,
};
use crate::{multimap::MultiMap, REF_ID_ATTRIBUTE_NAME, REF_POINTER_ATTRIBUTE_PREFIX};
+pub struct RefLinks {
+ /// A map of referents to each of their Ref properties.
+ prop_links: MultiMap[,
+ /// A set of referents that need their ID rewritten. This includes
+ /// Instances that have no existing ID.
+ need_rewrite: Vec][,
+}
+
#[derive(PartialEq, Eq)]
-pub struct RefLink {
+struct RefLink {
/// The name of a property
name: String,
/// The value of the property.
@@ -22,7 +30,9 @@ pub struct RefLink {
///
/// They can be linked to a dom later using the `link` method on the returned
/// struct.
-pub fn collect_referents(dom: &WeakDom) -> anyhow::Result> {
+pub fn collect_referents(dom: &WeakDom) -> RefLinks {
+ let mut existing_ids = HashMap::new();
+ let mut need_rewrite = Vec::new();
let mut links = MultiMap::new();
let mut queue = VecDeque::new();
@@ -30,98 +40,169 @@ pub fn collect_referents(dom: &WeakDom) -> anyhow::Result
// Note that this is back-in, front-out. This is important because
// VecDeque::extend is the equivalent to using push_back.
queue.push_back(dom.root_ref());
- while let Some(referent) = queue.pop_front() {
- let instance = dom.get_by_ref(referent).unwrap();
+ while let Some(inst_ref) = queue.pop_front() {
+ let instance = dom.get_by_ref(inst_ref).unwrap();
queue.extend(instance.children().iter().copied());
- for (name, value) in &instance.properties {
- if let Variant::Ref(prop_value) = value {
- if dom.get_by_ref(*prop_value).is_some() {
- links.insert(
- referent,
- RefLink {
- name: name.clone(),
- value: *prop_value,
- },
- );
+ // Collect all referent properties for easy access later
+ for (property_name, prop_value) in &instance.properties {
+ if let Variant::Ref(prop_ref) = prop_value {
+ // Any Instance that's pointed to as a property needs an ID.
+ let existing_id = match dom.get_by_ref(*prop_ref) {
+ Some(inst) => get_existing_id(inst),
+ None => continue,
+ };
+ if let Some(existing_id) = existing_id {
+ match existing_ids.entry(existing_id) {
+ Entry::Occupied(entry) => {
+ if entry.get() != prop_ref {
+ need_rewrite.push(*prop_ref);
+ }
+ }
+ Entry::Vacant(entry) => {
+ entry.insert(*prop_ref);
+ }
+ }
+ } else {
+ need_rewrite.push(*prop_ref);
}
+ // We also need a list of these properties for linking later
+ links.insert(
+ inst_ref,
+ RefLink {
+ name: property_name.to_owned(),
+ value: *prop_ref,
+ },
+ );
}
}
}
- Ok(links)
+ RefLinks {
+ prop_links: links,
+ need_rewrite,
+ }
}
-pub fn link_referents(link_list: MultiMap][, dom: &mut WeakDom) -> anyhow::Result<()> {
- let mut pointer_attributes = HashMap::new();
- for (pointer_ref, ref_properties) in link_list {
- if dom.get_by_ref(pointer_ref).is_none() {
- continue;
- }
+pub fn link_referents(links: RefLinks, dom: &mut WeakDom) -> anyhow::Result<()> {
+ write_id_attributes(&links, dom)?;
+
+ let mut prop_list = Vec::new();
+ let mut attribute_list = HashMap::new();
- // In this loop, we need to add the `Rojo_Id` attributes to the
- // Instances.
- for ref_link in ref_properties {
- let target_inst = match dom.get_by_ref_mut(ref_link.value) {
+ for (inst_id, properties) in links.prop_links {
+ for ref_link in properties {
+ let prop_inst = match dom.get_by_ref(ref_link.value) {
Some(inst) => inst,
- None => {
- continue;
- }
+ None => continue,
};
- pointer_attributes.insert(ref_link.name, get_or_insert_id(target_inst)?);
+ let id = get_existing_id(prop_inst)
+ .expect("all Instances that are pointed to should have an ID");
+ prop_list.push((ref_link.name, Variant::String(id.to_owned())));
}
+ let inst = match dom.get_by_ref_mut(inst_id) {
+ Some(inst) => inst,
+ None => continue,
+ };
- let pointer_inst = dom.get_by_ref_mut(pointer_ref).unwrap();
- let pointer_attrs = get_or_insert_attributes(pointer_inst)?;
- for (name, id) in pointer_attributes.drain() {
- pointer_attrs.insert(
- format!("{REF_POINTER_ATTRIBUTE_PREFIX}{name}"),
- Variant::BinaryString(id.into_bytes().into()),
+ // TODO: Replace this whole rigamarole with `Attributes::drain`
+ // eventually.
+ let attributes = match inst.properties.remove("Attributes") {
+ Some(Variant::Attributes(attrs)) => attrs,
+ None => Attributes::new(),
+ Some(value) => {
+ anyhow::bail!(
+ "expected Attributes to be of type 'Attributes' but it was of type '{:?}'",
+ value.ty()
+ );
+ }
+ };
+ for (name, value) in attributes.into_iter() {
+ if !name.starts_with(REF_POINTER_ATTRIBUTE_PREFIX) {
+ attribute_list.insert(name, value);
+ }
+ }
+
+ for (prop_name, prop_value) in prop_list.drain(..) {
+ attribute_list.insert(
+ format!("{REF_POINTER_ATTRIBUTE_PREFIX}{prop_name}"),
+ prop_value,
);
}
+
+ // TODO: Same as above, when `Attributes::drain` is live, replace this
+ // with it.
+ inst.properties.insert(
+ "Attributes".into(),
+ Attributes::from_iter(attribute_list.drain()).into(),
+ );
}
Ok(())
}
-fn get_or_insert_attributes(inst: &mut Instance) -> anyhow::Result<&mut Attributes> {
- if !inst.properties.contains_key("Attributes") {
- inst.properties
- .insert("Attributes".into(), Attributes::new().into());
- }
- match inst.properties.get_mut("Attributes") {
- Some(Variant::Attributes(attrs)) => Ok(attrs),
- Some(ty) => Err(anyhow::format_err!(
- "expected property Attributes to be an Attributes but it was {:?}",
- ty.ty()
- )),
- None => unreachable!(),
+fn write_id_attributes(links: &RefLinks, dom: &mut WeakDom) -> anyhow::Result<()> {
+ for referent in &links.need_rewrite {
+ let inst = match dom.get_by_ref_mut(*referent) {
+ Some(inst) => inst,
+ None => continue,
+ };
+ let unique_id = match inst.properties.get("UniqueId") {
+ Some(Variant::UniqueId(id)) => Some(*id),
+ _ => None,
+ }
+ .unwrap_or_else(|| UniqueId::now().unwrap());
+
+ let attributes = match inst.properties.get_mut("Attributes") {
+ Some(Variant::Attributes(attrs)) => attrs,
+ None => {
+ inst.properties
+ .insert("Attributes".into(), Attributes::new().into());
+ match inst.properties.get_mut("Attributes") {
+ Some(Variant::Attributes(attrs)) => attrs,
+ _ => unreachable!(),
+ }
+ }
+ Some(value) => {
+ anyhow::bail!(
+ "expected Attributes to be of type 'Attributes' but it was of type '{:?}'",
+ value.ty()
+ );
+ }
+ };
+ attributes.insert(
+ REF_ID_ATTRIBUTE_NAME.into(),
+ Variant::String(unique_id.to_string()),
+ );
}
+ Ok(())
}
-fn get_or_insert_id(inst: &mut Instance) -> anyhow::Result {
- let unique_id = match inst.properties.get("UniqueId") {
- Some(Variant::UniqueId(id)) => Some(*id),
- _ => None,
- };
- let referent = inst.referent();
- let attributes = get_or_insert_attributes(inst)?;
- match attributes.get(REF_ID_ATTRIBUTE_NAME) {
- Some(Variant::String(str)) => return Ok(str.clone()),
- Some(Variant::BinaryString(bytes)) => match std::str::from_utf8(bytes.as_ref()) {
- Ok(str) => return Ok(str.to_string()),
- Err(_) => {
- anyhow::bail!("expected attribute {REF_ID_ATTRIBUTE_NAME} to be a UTF-8 string")
- }
- },
- _ => {}
+fn get_existing_id(inst: &Instance) -> Option<&str> {
+ if let Variant::Attributes(attrs) = inst.properties.get("Attributes")? {
+ let id = attrs.get(REF_ID_ATTRIBUTE_NAME)?;
+ match id {
+ Variant::String(str) => Some(str),
+ Variant::BinaryString(bstr) => match std::str::from_utf8(bstr.as_ref()) {
+ Ok(str) => Some(str),
+ Err(_) => None,
+ },
+ _ => None,
+ }
+ } else {
+ None
}
- let id_string = unique_id
- .map(|id| id.to_string())
- .unwrap_or_else(|| referent.to_string());
- attributes.insert(
- REF_ID_ATTRIBUTE_NAME.to_string(),
- Variant::BinaryString(id_string.clone().into_bytes().into()),
- );
- Ok(id_string)
}
+
+/*
+When loading IDs we need to create a list and if there's a collision,
+cause it to have a stroke. If that works to catch the duplicates then we just
+need to re-serialize the IDs that collide. I think it's probably acceptable to
+just pick one of the two if we can't tell the difference between them and give
+the other a new ID. Maybe we emit a warning.
+
+This is gonna require a redo of the linking a bit, since it'll mean that we
+can't just blindly use the old IDs. The plus side is that it means we can
+collect these in a way that isn't awful via mapping an ID to a referent, so
+it may end up improving the overall quality of the above code.
+*/
diff --git a/src/syncback/snapshot.rs b/src/syncback/snapshot.rs
index 5fbdce738..178880931 100644
--- a/src/syncback/snapshot.rs
+++ b/src/syncback/snapshot.rs
@@ -5,7 +5,6 @@ use std::{
};
use crate::{
- glob::Glob,
snapshot::{InstanceWithMeta, RojoTree},
snapshot_middleware::Middleware,
Project,
@@ -101,13 +100,15 @@ impl<'sync> SyncbackSnapshot<'sync> {
}
/// Returns a map of properties for an Instance from the 'new' tree
- /// with filtering done to avoid noise. Returns `None` only if `new_ref`
- /// instance is not in the new tree.
+ /// with filtering done to avoid noise. This method filters out properties
+ /// that are not meant to be present in Instances that are represented
+ /// specially by a path, like `LocalScript.Source` and `StringValue.Value`.
///
- /// This method is not necessary or desired for blobs like RBXM or RBXMX.
+ /// This method is not necessary or desired for blobs like Rbxm or non-path
+ /// middlewares like JsonModel.
#[inline]
#[must_use]
- pub fn get_filtered_properties(
+ pub fn get_path_filtered_properties(
&self,
new_ref: Ref,
) -> Option> {
diff --git a/tests/tests/syncback.rs b/tests/tests/syncback.rs
index 529cf7e09..b51562953 100644
--- a/tests/tests/syncback.rs
+++ b/tests/tests/syncback.rs
@@ -24,4 +24,8 @@ syncback_basic_test! {
ref_properties_blank,
ref_properties_update,
ignore_paths,
+ project_reserialize,
+ project_all_middleware,
+ duplicate_rojo_id,
+ string_value_project,
}
]