diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ccc6752e..eb992a72b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,11 @@
# Rojo Changelog
## Unreleased Changes
+* Made the `name` field optional on project files ([#870])
+ Files named `default.project.json` inherit the name of the folder they're in and all other projects
+ are named as expect (e.g. `foo.project.json` becomes an Instance named `foo`)
+
+ There is no change in behavior if `name` is set.
* Fixed incorrect results when serving model pivots ([#868])
* Rojo now converts any line endings to LF, preventing spurious diffs when syncing Lua files on Windows ([#854])
* Fixed Rojo plugin failing to connect when project contains certain unreadable properties ([#848])
@@ -63,6 +68,7 @@
[#848]: https://github.com/rojo-rbx/rojo/pull/848
[#854]: https://github.com/rojo-rbx/rojo/pull/854
[#868]: https://github.com/rojo-rbx/rojo/pull/868
+[#870]: https://github.com/rojo-rbx/rojo/pull/870
## [7.4.0] - January 16, 2024
diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__no_name_default_project.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__no_name_default_project.snap
new file mode 100644
index 000000000..fbbd49116
--- /dev/null
+++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__no_name_default_project.snap
@@ -0,0 +1,22 @@
+---
+source: tests/tests/build.rs
+expression: contents
+---
+
+ -
+
+ top-level
+
+
-
+
+ second-level
+
+
-
+
+ third-level
+ 1337
+
+
+
+
+
diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__no_name_project.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__no_name_project.snap
new file mode 100644
index 000000000..588979334
--- /dev/null
+++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__no_name_project.snap
@@ -0,0 +1,22 @@
+---
+source: tests/tests/build.rs
+expression: contents
+---
+
+ -
+
+ no_name_project
+
+
-
+
+ second-level
+
+
-
+
+ bool_value
+ true
+
+
+
+
+
diff --git a/rojo-test/build-test-snapshots/end_to_end__tests__build__no_name_top_level_project.snap b/rojo-test/build-test-snapshots/end_to_end__tests__build__no_name_top_level_project.snap
new file mode 100644
index 000000000..2f2e3d6ad
--- /dev/null
+++ b/rojo-test/build-test-snapshots/end_to_end__tests__build__no_name_top_level_project.snap
@@ -0,0 +1,13 @@
+---
+source: tests/tests/build.rs
+assertion_line: 107
+expression: contents
+---
+
+ -
+
+ no_name_top_level_project
+ If this isn't named `no_name_top_level_project`, something went wrong!
+
+
+
diff --git a/rojo-test/build-tests/no_name_default_project/default.project.json b/rojo-test/build-tests/no_name_default_project/default.project.json
new file mode 100644
index 000000000..1c2a1400d
--- /dev/null
+++ b/rojo-test/build-tests/no_name_default_project/default.project.json
@@ -0,0 +1,9 @@
+{
+ "name": "top-level",
+ "tree": {
+ "$className": "Folder",
+ "second-level": {
+ "$path": "src"
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/build-tests/no_name_default_project/src/third-level/default.project.json b/rojo-test/build-tests/no_name_default_project/src/third-level/default.project.json
new file mode 100644
index 000000000..e7b87be1c
--- /dev/null
+++ b/rojo-test/build-tests/no_name_default_project/src/third-level/default.project.json
@@ -0,0 +1,8 @@
+{
+ "tree": {
+ "$className": "IntValue",
+ "$properties": {
+ "Value": 1337
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/build-tests/no_name_project/default.project.json b/rojo-test/build-tests/no_name_project/default.project.json
new file mode 100644
index 000000000..93ef2e8ce
--- /dev/null
+++ b/rojo-test/build-tests/no_name_project/default.project.json
@@ -0,0 +1,9 @@
+{
+ "name": "no_name_project",
+ "tree": {
+ "$className": "Folder",
+ "second-level": {
+ "$path": "src"
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/build-tests/no_name_project/src/bool_value.project.json b/rojo-test/build-tests/no_name_project/src/bool_value.project.json
new file mode 100644
index 000000000..6be58a3ce
--- /dev/null
+++ b/rojo-test/build-tests/no_name_project/src/bool_value.project.json
@@ -0,0 +1,8 @@
+{
+ "tree": {
+ "$className": "BoolValue",
+ "$properties": {
+ "Value": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/build-tests/no_name_top_level_project/default.project.json b/rojo-test/build-tests/no_name_top_level_project/default.project.json
new file mode 100644
index 000000000..845ae3725
--- /dev/null
+++ b/rojo-test/build-tests/no_name_top_level_project/default.project.json
@@ -0,0 +1,8 @@
+{
+ "tree": {
+ "$className": "StringValue",
+ "$properties": {
+ "Value": "If this isn't named `no_name_top_level_project`, something went wrong!"
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_default_project_all.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_default_project_all.snap
new file mode 100644
index 000000000..a8179a919
--- /dev/null
+++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_default_project_all.snap
@@ -0,0 +1,39 @@
+---
+source: tests/tests/serve.rs
+expression: "read_response.intern_and_redact(&mut redactions, root_id)"
+---
+instances:
+ id-2:
+ Children:
+ - id-3
+ ClassName: Folder
+ Id: id-2
+ Metadata:
+ ignoreUnknownInstances: true
+ Name: top-level
+ Parent: "00000000000000000000000000000000"
+ Properties: {}
+ id-3:
+ Children:
+ - id-4
+ ClassName: Folder
+ Id: id-3
+ Metadata:
+ ignoreUnknownInstances: false
+ Name: second-level
+ Parent: id-2
+ Properties: {}
+ id-4:
+ Children: []
+ ClassName: IntValue
+ Id: id-4
+ Metadata:
+ ignoreUnknownInstances: true
+ Name: third-level
+ Parent: id-3
+ Properties:
+ Value:
+ Int64: 1337
+messageCursor: 0
+sessionId: id-1
+
diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_default_project_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_default_project_info.snap
new file mode 100644
index 000000000..954ba6229
--- /dev/null
+++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_default_project_info.snap
@@ -0,0 +1,14 @@
+---
+source: tests/tests/serve.rs
+assertion_line: 316
+expression: redactions.redacted_yaml(info)
+---
+expectedPlaceIds: ~
+gameId: ~
+placeId: ~
+projectName: top-level
+protocolVersion: 4
+rootInstanceId: id-2
+serverVersion: "[server-version]"
+sessionId: id-1
+
diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_project_all.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_project_all.snap
new file mode 100644
index 000000000..1b398a928
--- /dev/null
+++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_project_all.snap
@@ -0,0 +1,40 @@
+---
+source: tests/tests/serve.rs
+assertion_line: 338
+expression: "read_response.intern_and_redact(&mut redactions, root_id)"
+---
+instances:
+ id-2:
+ Children:
+ - id-3
+ ClassName: Folder
+ Id: id-2
+ Metadata:
+ ignoreUnknownInstances: true
+ Name: no_name_project
+ Parent: "00000000000000000000000000000000"
+ Properties: {}
+ id-3:
+ Children:
+ - id-4
+ ClassName: Folder
+ Id: id-3
+ Metadata:
+ ignoreUnknownInstances: false
+ Name: second-level
+ Parent: id-2
+ Properties: {}
+ id-4:
+ Children: []
+ ClassName: BoolValue
+ Id: id-4
+ Metadata:
+ ignoreUnknownInstances: true
+ Name: bool_value
+ Parent: id-3
+ Properties:
+ Value:
+ Bool: true
+messageCursor: 0
+sessionId: id-1
+
diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_project_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_project_info.snap
new file mode 100644
index 000000000..d9772de1c
--- /dev/null
+++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_project_info.snap
@@ -0,0 +1,14 @@
+---
+source: tests/tests/serve.rs
+assertion_line: 335
+expression: redactions.redacted_yaml(info)
+---
+expectedPlaceIds: ~
+gameId: ~
+placeId: ~
+projectName: no_name_project
+protocolVersion: 4
+rootInstanceId: id-2
+serverVersion: "[server-version]"
+sessionId: id-1
+
diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_top_level_project_all.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_top_level_project_all.snap
new file mode 100644
index 000000000..ea52fe4bf
--- /dev/null
+++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_top_level_project_all.snap
@@ -0,0 +1,20 @@
+---
+source: tests/tests/serve.rs
+assertion_line: 357
+expression: "read_response.intern_and_redact(&mut redactions, root_id)"
+---
+instances:
+ id-2:
+ Children: []
+ ClassName: StringValue
+ Id: id-2
+ Metadata:
+ ignoreUnknownInstances: true
+ Name: no_name_top_level_project
+ Parent: "00000000000000000000000000000000"
+ Properties:
+ Value:
+ String: "If this isn't named `no_name_top_level_project`, something went wrong!"
+messageCursor: 0
+sessionId: id-1
+
diff --git a/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_top_level_project_info.snap b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_top_level_project_info.snap
new file mode 100644
index 000000000..3ad79c37e
--- /dev/null
+++ b/rojo-test/serve-test-snapshots/end_to_end__tests__serve__no_name_top_level_project_info.snap
@@ -0,0 +1,14 @@
+---
+source: tests/tests/serve.rs
+assertion_line: 351
+expression: redactions.redacted_yaml(info)
+---
+expectedPlaceIds: ~
+gameId: ~
+placeId: ~
+projectName: no_name_top_level_project
+protocolVersion: 4
+rootInstanceId: id-2
+serverVersion: "[server-version]"
+sessionId: id-1
+
diff --git a/rojo-test/serve-tests/no_name_default_project/default.project.json b/rojo-test/serve-tests/no_name_default_project/default.project.json
new file mode 100644
index 000000000..1c2a1400d
--- /dev/null
+++ b/rojo-test/serve-tests/no_name_default_project/default.project.json
@@ -0,0 +1,9 @@
+{
+ "name": "top-level",
+ "tree": {
+ "$className": "Folder",
+ "second-level": {
+ "$path": "src"
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/serve-tests/no_name_default_project/src/third-level/default.project.json b/rojo-test/serve-tests/no_name_default_project/src/third-level/default.project.json
new file mode 100644
index 000000000..e7b87be1c
--- /dev/null
+++ b/rojo-test/serve-tests/no_name_default_project/src/third-level/default.project.json
@@ -0,0 +1,8 @@
+{
+ "tree": {
+ "$className": "IntValue",
+ "$properties": {
+ "Value": 1337
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/serve-tests/no_name_project/default.project.json b/rojo-test/serve-tests/no_name_project/default.project.json
new file mode 100644
index 000000000..93ef2e8ce
--- /dev/null
+++ b/rojo-test/serve-tests/no_name_project/default.project.json
@@ -0,0 +1,9 @@
+{
+ "name": "no_name_project",
+ "tree": {
+ "$className": "Folder",
+ "second-level": {
+ "$path": "src"
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/serve-tests/no_name_project/src/bool_value.project.json b/rojo-test/serve-tests/no_name_project/src/bool_value.project.json
new file mode 100644
index 000000000..6be58a3ce
--- /dev/null
+++ b/rojo-test/serve-tests/no_name_project/src/bool_value.project.json
@@ -0,0 +1,8 @@
+{
+ "tree": {
+ "$className": "BoolValue",
+ "$properties": {
+ "Value": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/rojo-test/serve-tests/no_name_top_level_project/default.project.json b/rojo-test/serve-tests/no_name_top_level_project/default.project.json
new file mode 100644
index 000000000..845ae3725
--- /dev/null
+++ b/rojo-test/serve-tests/no_name_top_level_project/default.project.json
@@ -0,0 +1,8 @@
+{
+ "tree": {
+ "$className": "StringValue",
+ "$properties": {
+ "Value": "If this isn't named `no_name_top_level_project`, something went wrong!"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/project.rs b/src/project.rs
index 1095fba20..bf6bc163d 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -39,7 +39,7 @@ enum Error {
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct Project {
/// The name of the top-level instance described by the project.
- pub name: String,
+ pub name: Option,
/// The tree of instances described by this project. Projects always
/// describe at least one instance.
diff --git a/src/serve_session.rs b/src/serve_session.rs
index 85ae77934..dbc841d74 100644
--- a/src/serve_session.rs
+++ b/src/serve_session.rs
@@ -110,7 +110,7 @@ impl ServeSession {
log::debug!("Loading project file from {}", project_path.display());
- let root_project = match vfs.read(&project_path).with_not_found()? {
+ let mut root_project = match vfs.read(&project_path).with_not_found()? {
Some(contents) => Project::load_from_slice(&contents, &project_path)?,
None => {
return Err(ServeSessionError::NoProjectFound {
@@ -118,6 +118,35 @@ impl ServeSession {
});
}
};
+ if root_project.name.is_none() {
+ if let Some(file_name) = project_path.file_name().and_then(|s| s.to_str()) {
+ if file_name == "default.project.json" {
+ let folder_name = project_path
+ .parent()
+ .and_then(Path::file_name)
+ .and_then(|s| s.to_str());
+ if let Some(folder_name) = folder_name {
+ root_project.name = Some(folder_name.to_string());
+ } else {
+ return Err(ServeSessionError::FolderNameInvalid {
+ path: project_path.to_path_buf(),
+ });
+ }
+ } else if let Some(trimmed) = file_name.strip_suffix(".project.json") {
+ root_project.name = Some(trimmed.to_string());
+ } else {
+ return Err(ServeSessionError::ProjectNameInvalid {
+ path: project_path.to_path_buf(),
+ });
+ }
+ } else {
+ return Err(ServeSessionError::ProjectNameInvalid {
+ path: project_path.to_path_buf(),
+ });
+ }
+ }
+ // Rebind it to make it no longer mutable
+ let root_project = root_project;
let mut tree = RojoTree::new(InstanceSnapshot::new());
@@ -190,7 +219,10 @@ impl ServeSession {
}
pub fn project_name(&self) -> &str {
- &self.root_project.name
+ self.root_project
+ .name
+ .as_ref()
+ .expect("all top-level projects must have their name set")
}
pub fn project_port(&self) -> Option {
@@ -231,6 +263,14 @@ pub enum ServeSessionError {
)]
NoProjectFound { path: PathBuf },
+ #[error("The folder for the provided project cannot be used as a project name: {}\n\
+ Consider setting the `name` field on this project.", .path.display())]
+ FolderNameInvalid { path: PathBuf },
+
+ #[error("The file name of the provided project cannot be used as a project name: {}.\n\
+ Consider setting the `name` field on this project.", .path.display())]
+ ProjectNameInvalid { path: PathBuf },
+
#[error(transparent)]
Io {
#[from]
diff --git a/src/snapshot_middleware/mod.rs b/src/snapshot_middleware/mod.rs
index 30ea57061..1c95154ef 100644
--- a/src/snapshot_middleware/mod.rs
+++ b/src/snapshot_middleware/mod.rs
@@ -66,7 +66,13 @@ pub fn snapshot_from_vfs(
for rule in default_sync_rules() {
if rule.matches(&init_path) {
return match rule.middleware {
- Middleware::Project => snapshot_project(context, vfs, &init_path),
+ Middleware::Project => {
+ let name = init_path
+ .parent()
+ .and_then(Path::file_name)
+ .and_then(|s| s.to_str()).expect("default.project.json should be inside a folder with a unicode name");
+ snapshot_project(context, vfs, &init_path, name)
+ }
Middleware::ModuleScript => {
snapshot_lua_init(context, vfs, &init_path, ScriptType::Module)
@@ -218,9 +224,7 @@ impl Middleware {
Self::ServerScript => snapshot_lua(context, vfs, path, name, ScriptType::Server),
Self::ClientScript => snapshot_lua(context, vfs, path, name, ScriptType::Client),
Self::ModuleScript => snapshot_lua(context, vfs, path, name, ScriptType::Module),
- // At the moment, snapshot_project does not use `name` so we
- // don't provide it.
- Self::Project => snapshot_project(context, vfs, path),
+ Self::Project => snapshot_project(context, vfs, path, name),
Self::Rbxm => snapshot_rbxm(context, vfs, path, name),
Self::Rbxmx => snapshot_rbxmx(context, vfs, path, name),
Self::Toml => snapshot_toml(context, vfs, path, name),
@@ -280,8 +284,7 @@ fn default_sync_rules() -> &'static [SyncRule] {
sync_rule!("*.client.lua", ClientScript, ".client.lua"),
sync_rule!("*.client.luau", ClientScript, ".client.luau"),
sync_rule!("*.{lua,luau}", ModuleScript),
- // Project middleware doesn't use the file name.
- sync_rule!("*.project.json", Project),
+ sync_rule!("*.project.json", Project, ".project.json"),
sync_rule!("*.model.json", JsonModel, ".model.json"),
sync_rule!("*.json", Json, ".json", "*.meta.json"),
sync_rule!("*.toml", Toml),
diff --git a/src/snapshot_middleware/project.rs b/src/snapshot_middleware/project.rs
index c92f42d8a..31ca7dc2d 100644
--- a/src/snapshot_middleware/project.rs
+++ b/src/snapshot_middleware/project.rs
@@ -19,9 +19,11 @@ pub fn snapshot_project(
context: &InstanceContext,
vfs: &Vfs,
path: &Path,
+ name: &str,
) -> anyhow::Result