Compare commits

...

11 Commits

Author SHA1 Message Date
Micah
5bd3c74db0 Release Rojo v7.4.4 (#964) 2024-08-22 10:06:13 -07:00
Micah
f4e2f5aefc Release Rojo 7.4.3 (#959) 2024-08-06 11:26:00 -07:00
Micah
8ceb40a24e Update rbx-binary to 0.7.6 (#958) 2024-08-06 11:21:41 -07:00
Micah
3e53d67412 In the plugin, don't write properties if they're nil and also a number (#955) 2024-08-02 10:02:32 -07:00
Micah
844f51d916 Update version of aftman used in release workflow 2024-07-23 11:14:18 -07:00
Micah
26974ffd4c Release v7.4.2 (#950) 2024-07-23 11:02:23 -07:00
Micah
91f5b4a675 Update memofs in 7.4.x branch (#949)
Backports the release of memofs v0.3.0
2024-07-23 10:42:32 -07:00
Micah
d179240139 Update rbx_dom for 7.4.x branch (#948) 2024-07-23 10:35:06 -07:00
Micah
67b6a7e198 Backport #917 to Rojo 7.4.x branch (#947) 2024-07-22 12:11:28 -07:00
Micah
3b721242c1 Backport #893 and #903 to Rojo 7.4 (#946)
As part of prep for a 7.4.2 release, this backports changes to the 7.4.X
branch that we can reasonably ship in 7.4.2 without too many code
changes.
2024-07-22 11:55:28 -07:00
EgoMoose
c6ceaa5c87 Trim plugin version string (#889)
This PR is a very small change that fixes the string pattern that reads
the rojo version from `Version.txt`. Currently this reads an extra
new-line character which makes reading the version text in the plugin
difficult.

It seems the rust side of things already trims the string when
comparing, but the lua side does not.

Current:

![pO6gtOXAZq](https://github.com/rojo-rbx/rojo/assets/6201941/1a03fced-f2b5-4a4e-a82d-e11fb0a52af7)

Fix:

![RobloxStudioBeta_GHmiJKAoa3](https://github.com/rojo-rbx/rojo/assets/6201941/3ce711df-fdc6-4f20-8771-5f5118ee013f)

Apologies if I skipped over some process of submitting a bug and / or am
basing on the wrong branch etc.
2024-03-13 09:49:33 -07:00
22 changed files with 11661 additions and 1310 deletions

View File

@@ -36,7 +36,7 @@ jobs:
with: with:
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
trust-check: false trust-check: false
version: 'v0.2.6' version: 'v0.3.0'
- name: Build Plugin - name: Build Plugin
run: rojo build plugin --output Rojo.rbxm run: rojo build plugin --output Rojo.rbxm

View File

@@ -2,6 +2,27 @@
## Unreleased Changes ## Unreleased Changes
## [7.4.4] - August 22nd, 2024
* Fixed issue with reading attributes from `Lighting` in new place files
* `Instance.Archivable` will now default to `true` when building a project into a binary (`rbxm`/`rbxl`) file rather than `false`.
## [7.4.3] - August 6th, 2024
* Fixed issue with building binary files introduced in 7.4.2
* Fixed `value of type nil cannot be converted to number` warning spam in output. [#955]
[#955]: https://github.com/rojo-rbx/rojo/pull/893
## [7.4.2] - July 23, 2024
* Added Never option to Confirmation ([#893])
* Fixed removing trailing newlines ([#903])
* Updated the internal property database, correcting an issue with `SurfaceAppearance.Color` that was reported [here][Surface_Appearance_Color_1] and [here][Surface_Appearance_Color_2] ([#948])
[#893]: https://github.com/rojo-rbx/rojo/pull/893
[#903]: https://github.com/rojo-rbx/rojo/pull/903
[#948]: https://github.com/rojo-rbx/rojo/pull/948
[Surface_Appearance_Color_1]: https://devforum.roblox.com/t/jailbreak-custom-character-turned-shiny-black-no-texture/3075563
[Surface_Appearance_Color_2]: https://devforum.roblox.com/t/surfaceappearance-not-displaying-correctly/3075588
## [7.4.1] - February 20, 2024 ## [7.4.1] - February 20, 2024
* Made the `name` field optional on project files ([#870]) * 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 Files named `default.project.json` inherit the name of the folder they're in and all other projects

28
Cargo.lock generated
View File

@@ -1073,7 +1073,7 @@ dependencies = [
[[package]] [[package]]
name = "memofs" name = "memofs"
version = "0.2.0" version = "0.3.0"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"fs-err", "fs-err",
@@ -1586,9 +1586,9 @@ dependencies = [
[[package]] [[package]]
name = "rbx_binary" name = "rbx_binary"
version = "0.7.4" version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6314dd6bf5c21d0598cdb53cf5d241aa643ba41da8b8abf7402b4a35096f03f6" checksum = "7b85057e8ff75a1ce99248200c4b3c7b481a3d52f921f1053ecd67921dcc7930"
dependencies = [ dependencies = [
"log", "log",
"lz4", "lz4",
@@ -1601,9 +1601,9 @@ dependencies = [
[[package]] [[package]]
name = "rbx_dom_weak" name = "rbx_dom_weak"
version = "2.7.0" version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b67b56bac99849c2e3c57547b036927f71c57cf7f4d900d04e3e4ee774ec316" checksum = "fcd2a17d09e46af0805f8b311a926402172b97e8d9388745c9adf8f448901841"
dependencies = [ dependencies = [
"rbx_types", "rbx_types",
"serde", "serde",
@@ -1611,9 +1611,9 @@ dependencies = [
[[package]] [[package]]
name = "rbx_reflection" name = "rbx_reflection"
version = "4.5.0" version = "4.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d41509c991b53a7276a746a795eae2b9204f398164920f61976995b47fe1722" checksum = "8118ac6021d700e8debe324af6b40ecfd2cef270a00247849dbdfeebb0802677"
dependencies = [ dependencies = [
"rbx_types", "rbx_types",
"serde", "serde",
@@ -1622,9 +1622,9 @@ dependencies = [
[[package]] [[package]]
name = "rbx_reflection_database" name = "rbx_reflection_database"
version = "0.2.10+roblox-607" version = "0.2.12+roblox-638"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12e20c06fa41f7aadc79005c8354f592b2c2f4d0c61e1080ed5718dafc30aea0" checksum = "0e29381d675420e841f8c02db5755cbb2545ed3e13f56c539546dc58702b512a"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"rbx_reflection", "rbx_reflection",
@@ -1634,9 +1634,9 @@ dependencies = [
[[package]] [[package]]
name = "rbx_types" name = "rbx_types"
version = "1.8.0" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ca23bfd469d067d81ef14f65fe09aeddc25abcf576a889d1a7664fe021cf18c" checksum = "e30f49b2a3bb667e4074ba73c2dfb8ca0873f610b448ccf318a240acfdec6c73"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"bitflags 1.3.2", "bitflags 1.3.2",
@@ -1649,9 +1649,9 @@ dependencies = [
[[package]] [[package]]
name = "rbx_xml" name = "rbx_xml"
version = "0.13.3" version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c03f95500961c32340791d1fabd4587f6873bdbff077ecca6ae32db7960dea" checksum = "2b14b3027bc9ccd82e2fc854c8bcd25ed58318e570c355bf2cf63df9cdbd5ba8"
dependencies = [ dependencies = [
"base64 0.13.1", "base64 0.13.1",
"log", "log",
@@ -1831,7 +1831,7 @@ dependencies = [
[[package]] [[package]]
name = "rojo" name = "rojo"
version = "7.4.1" version = "7.4.4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"backtrace", "backtrace",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "rojo" name = "rojo"
version = "7.4.1" version = "7.4.4"
rust-version = "1.70.0" rust-version = "1.70.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"] authors = ["Lucien Greathouse <me@lpghatguy.com>"]
description = "Enables professional-grade development tools for Roblox developers" description = "Enables professional-grade development tools for Roblox developers"
@@ -40,7 +40,7 @@ name = "build"
harness = false harness = false
[dependencies] [dependencies]
memofs = { version = "0.2.0", path = "crates/memofs" } memofs = { version = "0.3.0", path = "crates/memofs" }
# These dependencies can be uncommented when working on rbx-dom simultaneously # These dependencies can be uncommented when working on rbx-dom simultaneously
# rbx_binary = { path = "../rbx-dom/rbx_binary" } # rbx_binary = { path = "../rbx-dom/rbx_binary" }
@@ -49,11 +49,11 @@ memofs = { version = "0.2.0", path = "crates/memofs" }
# rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" } # rbx_reflection_database = { path = "../rbx-dom/rbx_reflection_database" }
# rbx_xml = { path = "../rbx-dom/rbx_xml" } # rbx_xml = { path = "../rbx-dom/rbx_xml" }
rbx_binary = "0.7.4" rbx_binary = "0.7.7"
rbx_dom_weak = "2.7.0" rbx_dom_weak = "2.9.0"
rbx_reflection = "4.5.0" rbx_reflection = "4.7.0"
rbx_reflection_database = "0.2.10" rbx_reflection_database = "0.2.12"
rbx_xml = "0.13.3" rbx_xml = "0.13.5"
anyhow = "1.0.44" anyhow = "1.0.44"
backtrace = "0.3.61" backtrace = "0.3.61"
@@ -94,7 +94,7 @@ tracy-client = { version = "0.13.2", optional = true }
winreg = "0.10.1" winreg = "0.10.1"
[build-dependencies] [build-dependencies]
memofs = { version = "0.2.0", path = "crates/memofs" } memofs = { version = "0.3.0", path = "crates/memofs" }
embed-resource = "1.6.4" embed-resource = "1.6.4"
anyhow = "1.0.44" anyhow = "1.0.44"

View File

@@ -1,5 +1,5 @@
[tools] [tools]
rojo = "rojo-rbx/rojo@7.3.0" rojo = "rojo-rbx/rojo@7.4.1"
selene = "Kampfkarren/selene@0.26.1" selene = "Kampfkarren/selene@0.26.1"
stylua = "JohnnyMorganz/stylua@0.18.2" stylua = "JohnnyMorganz/stylua@0.18.2"
run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0" run-in-roblox = "rojo-rbx/run-in-roblox@0.3.0"

View File

@@ -1,6 +1,8 @@
# memofs Changelog # memofs Changelog
## Unreleased Changes ## Unreleased Changes
## 0.3.0 (2024-03-15)
* Changed `StdBackend` file watching component to use minimal recursive watches. [#830] * Changed `StdBackend` file watching component to use minimal recursive watches. [#830]
* Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854] * Added `Vfs::read_to_string` and `Vfs::read_to_string_lf_normalized` [#854]

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "memofs" name = "memofs"
description = "Virtual filesystem with configurable backends." description = "Virtual filesystem with configurable backends."
version = "0.2.0" version = "0.3.0"
authors = ["Lucien Greathouse <me@lpghatguy.com>"] authors = ["Lucien Greathouse <me@lpghatguy.com>"]
edition = "2018" edition = "2018"
readme = "README.md" readme = "README.md"

View File

@@ -300,7 +300,7 @@ impl Vfs {
let path = path.as_ref(); let path = path.as_ref();
let contents = self.inner.lock().unwrap().read_to_string(path)?; let contents = self.inner.lock().unwrap().read_to_string(path)?;
Ok(contents.lines().collect::<Vec<&str>>().join("\n").into()) Ok(contents.replace("\r\n", "\n").into())
} }
/// Write a file to the VFS and the underlying backend. /// Write a file to the VFS and the underlying backend.
@@ -473,3 +473,23 @@ impl VfsLock<'_> {
self.inner.commit_event(event) self.inner.commit_event(event)
} }
} }
#[cfg(test)]
mod test {
use crate::{InMemoryFs, Vfs, VfsSnapshot};
/// https://github.com/rojo-rbx/rojo/issues/899
#[test]
fn read_to_string_lf_normalized_keeps_trailing_newline() {
let mut imfs = InMemoryFs::new();
imfs.load_snapshot("test", VfsSnapshot::file("bar\r\nfoo\r\n\r\n"))
.unwrap();
let vfs = Vfs::new(imfs);
assert_eq!(
vfs.read_to_string_lf_normalized("test").unwrap().as_str(),
"bar\nfoo\n\n"
);
}
}

View File

@@ -1 +1 @@
7.4.1 7.4.4

View File

@@ -26,6 +26,21 @@ local TERRAIN_MATERIAL_COLORS = {
Enum.Material.Pavement, Enum.Material.Pavement,
} }
local function isAttributeNameValid(attributeName)
-- For SetAttribute to succeed, the attribute name must be less than or
-- equal to 100 characters...
return #attributeName <= 100
-- ...and must only contain alphanumeric characters, periods, hyphens,
-- underscores, or forward slashes.
and attributeName:match("[^%w%.%-_/]") == nil
end
local function isAttributeNameReserved(attributeName)
-- For SetAttribute to succeed, attribute names must not use the RBX
-- prefix, which is reserved by Roblox.
return attributeName:sub(1, 3) == "RBX"
end
-- Defines how to read and write properties that aren't directly scriptable. -- Defines how to read and write properties that aren't directly scriptable.
-- --
-- The reflection database refers to these as having scriptability = "Custom" -- The reflection database refers to these as having scriptability = "Custom"
@@ -40,26 +55,33 @@ return {
local didAllWritesSucceed = true local didAllWritesSucceed = true
for attributeName, attributeValue in pairs(value) do for attributeName, attributeValue in pairs(value) do
local isNameValid = if isAttributeNameReserved(attributeName) then
-- For our SetAttribute to succeed, the attribute name must be -- If the attribute name is reserved, then we don't
-- less than or equal to 100 characters... -- really care about reporting any failures about
#attributeName <= 100 -- it.
-- ...must only contain alphanumeric characters, periods, hyphens, continue
-- underscores, or forward slashes...
and attributeName:match("[^%w%.%-_/]") == nil
-- ... and must not use the RBX prefix, which is reserved by Roblox.
and attributeName:sub(1, 3) ~= "RBX"
if isNameValid then
instance:SetAttribute(attributeName, attributeValue)
else
didAllWritesSucceed = false
end end
if not isAttributeNameValid(attributeName) then
didAllWritesSucceed = false
continue
end
instance:SetAttribute(attributeName, attributeValue)
end end
for key in pairs(existing) do for existingAttributeName in pairs(existing) do
if value[key] == nil then if isAttributeNameReserved(existingAttributeName) then
instance:SetAttribute(key, nil) continue
end
if not isAttributeNameValid(existingAttributeName) then
didAllWritesSucceed = false
continue
end
if value[existingAttributeName] == nil then
instance:SetAttribute(existingAttributeName, nil)
end end
end end
@@ -113,13 +135,14 @@ return {
}, },
WorldPivotData = { WorldPivotData = {
read = function(instance) read = function(instance)
return true, instance:GetPivot() return true, instance.WorldPivot
end, end,
write = function(instance, _, value) write = function(instance, _, value)
if value == nil then if value == nil then
return true, nil return true, nil
else else
return true, instance:PivotTo(value) instance.WorldPivot = value
return true
end end
end, end,
}, },

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ local function invertTbl(tbl)
end end
local invertedLevels = invertTbl(Log.Level) local invertedLevels = invertTbl(Log.Level)
local confirmationBehaviors = { "Initial", "Always", "Large Changes", "Unlisted PlaceId" } local confirmationBehaviors = { "Initial", "Always", "Large Changes", "Unlisted PlaceId", "Never" }
local function Navbar(props) local function Navbar(props)
return Theme.with(function(theme) return Theme.with(function(theme)

View File

@@ -516,6 +516,9 @@ function App:startSession()
return "Accept" return "Accept"
end end
end end
elseif confirmationBehavior == "Never" then
Log.trace("Accepting patch without confirmation because behavior is set to Never")
return "Accept"
end end
-- The datamodel name gets overwritten by Studio, making confirmation of it intrusive -- The datamodel name gets overwritten by Studio, making confirmation of it intrusive

View File

@@ -3,7 +3,8 @@ local strict = require(script.Parent.strict)
local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil local isDevBuild = script.Parent.Parent:FindFirstChild("ROJO_DEV_BUILD") ~= nil
local Version = script.Parent.Parent.Version local Version = script.Parent.Parent.Version
local major, minor, patch, metadata = Version.Value:match("^(%d+)%.(%d+)%.(%d+)(.*)$") local trimmedVersionValue = Version.Value:gsub("^%s+", ""):gsub("%s+$", "")
local major, minor, patch, metadata = trimmedVersionValue:match("^(%d+)%.(%d+)%.(%d+)(.*)$")
local realVersion = { major, minor, patch, metadata } local realVersion = { major, minor, patch, metadata }
for i = 1, 3 do for i = 1, 3 do

View File

@@ -7,7 +7,7 @@ local Log = require(Packages.Log)
local RbxDom = require(Packages.RbxDom) local RbxDom = require(Packages.RbxDom)
local Error = require(script.Parent.Error) local Error = require(script.Parent.Error)
local function setProperty(instance, propertyName, value) local function setProperty(instance: Instance, propertyName: string, value: unknown): boolean
local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName) local descriptor = RbxDom.findCanonicalPropertyDescriptor(instance.ClassName, propertyName)
-- We can skip unknown properties; they're not likely reflected to Lua. -- We can skip unknown properties; they're not likely reflected to Lua.
@@ -28,6 +28,13 @@ local function setProperty(instance, propertyName, value)
}) })
end end
if value == nil then
if descriptor.dataType == "Float32" or descriptor.dataType == "Float64" then
Log.trace("Skipping nil {} property {}.{}", descriptor.dataType, instance.ClassName, propertyName)
return true
end
end
local writeSuccess, err = descriptor:write(instance, value) local writeSuccess, err = descriptor:write(instance, value)
if not writeSuccess then if not writeSuccess then

View File

@@ -0,0 +1,19 @@
---
source: tests/tests/serve.rs
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

View File

@@ -2,6 +2,7 @@ use std::path::PathBuf;
use anyhow::Context; use anyhow::Context;
use clap::Parser; use clap::Parser;
use memofs::Vfs;
use crate::project::Project; use crate::project::Project;
@@ -17,8 +18,11 @@ pub struct FmtProjectCommand {
impl FmtProjectCommand { impl FmtProjectCommand {
pub fn run(self) -> anyhow::Result<()> { pub fn run(self) -> anyhow::Result<()> {
let vfs = Vfs::new_default();
vfs.set_watch_enabled(false);
let base_path = resolve_path(&self.project); let base_path = resolve_path(&self.project);
let project = Project::load_fuzzy(&base_path)? let project = Project::load_fuzzy(&vfs, &base_path)?
.context("A project file is required to run 'rojo fmt-project'")?; .context("A project file is required to run 'rojo fmt-project'")?;
let serialized = serde_json::to_string_pretty(&project) let serialized = serde_json::to_string_pretty(&project)

View File

@@ -1,10 +1,12 @@
use std::{ use std::{
collections::{BTreeMap, HashMap, HashSet}, collections::{BTreeMap, HashMap, HashSet},
ffi::OsStr,
fs, io, fs, io,
net::IpAddr, net::IpAddr,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use memofs::Vfs;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
@@ -19,6 +21,14 @@ pub struct ProjectError(#[from] Error);
#[derive(Debug, Error)] #[derive(Debug, Error)]
enum Error { enum Error {
#[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)] #[error(transparent)]
Io { Io {
#[from] #[from]
@@ -129,43 +139,91 @@ impl Project {
} }
} }
pub fn load_from_slice( /// Sets the name of a project. The order it handles is as follows:
///
/// - If the project is a `default.project.json`, uses the folder's name
/// - If a fallback is specified, uses that blindly
/// - Otherwise, loops through sync rules (including the default ones!) and
/// uses the name of the first one that matches and is a project file
fn set_file_name(&mut self, fallback: Option<&str>) -> Result<(), Error> {
let file_name = self
.file_location
.file_name()
.and_then(OsStr::to_str)
.ok_or_else(|| Error::ProjectNameInvalid {
path: self.file_location.clone(),
})?;
// If you're editing this to be generic, make sure you also alter the
// snapshot middleware to support generic init paths.
if file_name == PROJECT_FILENAME {
let folder_name = self.folder_location().file_name().and_then(OsStr::to_str);
if let Some(folder_name) = folder_name {
self.name = Some(folder_name.to_string());
} else {
return Err(Error::FolderNameInvalid {
path: self.file_location.clone(),
});
}
} else if let Some(fallback) = fallback {
self.name = Some(fallback.to_string());
} else {
unimplemented!(
"7.4.X branch will hopefully never have a case where fallback isn't provided to set_file_name"
);
}
Ok(())
}
/// Loads a Project file from the provided contents with its source set as
/// the provided location.
fn load_from_slice(
contents: &[u8], contents: &[u8],
project_file_location: &Path, project_file_location: PathBuf,
) -> Result<Self, ProjectError> { fallback_name: Option<&str>,
) -> Result<Self, Error> {
let mut project: Self = serde_json::from_slice(contents).map_err(|source| Error::Json { let mut project: Self = serde_json::from_slice(contents).map_err(|source| Error::Json {
source, source,
path: project_file_location.to_owned(), path: project_file_location.clone(),
})?; })?;
project.file_location = project_file_location;
project.file_location = project_file_location.to_path_buf();
project.check_compatibility(); project.check_compatibility();
if project.name.is_none() {
project.set_file_name(fallback_name)?;
}
Ok(project) Ok(project)
} }
pub fn load_fuzzy(fuzzy_project_location: &Path) -> Result<Option<Self>, ProjectError> { /// Loads a Project from a path. This will find the project if it refers to
/// a `.project.json` file or if it refers to a directory that contains a
/// file named `default.project.json`.
pub fn load_fuzzy(
vfs: &Vfs,
fuzzy_project_location: &Path,
) -> Result<Option<Self>, ProjectError> {
if let Some(project_path) = Self::locate(fuzzy_project_location) { if let Some(project_path) = Self::locate(fuzzy_project_location) {
let project = Self::load_exact(&project_path)?; let contents = vfs.read(&project_path).map_err(Error::from)?;
Ok(Some(Self::load_from_slice(&contents, project_path, None)?))
Ok(Some(project))
} else { } else {
Ok(None) Ok(None)
} }
} }
fn load_exact(project_file_location: &Path) -> Result<Self, Error> { /// Loads a Project from a path.
let contents = fs::read_to_string(project_file_location)?; pub fn load_exact(
vfs: &Vfs,
let mut project: Project = project_file_location: &Path,
serde_json::from_str(&contents).map_err(|source| Error::Json { fallback_name: Option<&str>,
source, ) -> Result<Self, ProjectError> {
path: project_file_location.to_owned(), let project_path = project_file_location.to_path_buf();
})?; let contents = vfs.read(&project_path).map_err(Error::from)?;
Ok(Self::load_from_slice(
project.file_location = project_file_location.to_path_buf(); &contents,
project.check_compatibility(); project_path,
fallback_name,
Ok(project) )?)
} }
/// Checks if there are any compatibility issues with this project file and /// Checks if there are any compatibility issues with this project file and

View File

@@ -9,7 +9,6 @@ use std::{
}; };
use crossbeam_channel::Sender; use crossbeam_channel::Sender;
use memofs::IoResultExt;
use memofs::Vfs; use memofs::Vfs;
use thiserror::Error; use thiserror::Error;
@@ -110,43 +109,14 @@ impl ServeSession {
log::debug!("Loading project file from {}", project_path.display()); log::debug!("Loading project file from {}", project_path.display());
let mut root_project = match vfs.read(&project_path).with_not_found()? { let root_project = match Project::load_exact(&vfs, &project_path, None) {
Some(contents) => Project::load_from_slice(&contents, &project_path)?, Ok(project) => project,
None => { Err(_) => {
return Err(ServeSessionError::NoProjectFound { return Err(ServeSessionError::NoProjectFound {
path: project_path.to_path_buf(), path: project_path.to_path_buf(),
}); });
} }
}; };
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()); let mut tree = RojoTree::new(InstanceSnapshot::new());
@@ -263,14 +233,6 @@ pub enum ServeSessionError {
)] )]
NoProjectFound { path: PathBuf }, 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)] #[error(transparent)]
Io { Io {
#[from] #[from]

View File

@@ -1,4 +1,4 @@
use std::{borrow::Cow, collections::HashMap, path::Path}; use std::{borrow::Cow, collections::HashMap, ffi::OsStr, path::Path};
use anyhow::{bail, Context}; use anyhow::{bail, Context};
use memofs::Vfs; use memofs::Vfs;
@@ -19,7 +19,18 @@ pub fn snapshot_project(
vfs: &Vfs, vfs: &Vfs,
path: &Path, path: &Path,
) -> anyhow::Result<Option<InstanceSnapshot>> { ) -> anyhow::Result<Option<InstanceSnapshot>> {
let project = Project::load_from_slice(&vfs.read(path)?, path) let fallback_name = match path.file_name().and_then(OsStr::to_str) {
Some("default.project.json") => path
.parent()
.and_then(Path::file_name)
.and_then(OsStr::to_str),
Some(name) => name.strip_suffix(".project.json"),
None => anyhow::bail!(
"project file does not have valid utf-8 name: {}",
path.display()
),
};
let project = Project::load_exact(vfs, path, fallback_name)
.with_context(|| format!("File was not a valid Rojo project: {}", path.display()))?; .with_context(|| format!("File was not a valid Rojo project: {}", path.display()))?;
// This is not how I would normally do this, but this is a temporary // This is not how I would normally do this, but this is a temporary

View File

@@ -87,7 +87,7 @@ impl TestServeSession {
let port_string = port.to_string(); let port_string = port.to_string();
let rojo_process = Command::new(ROJO_PATH) let rojo_process = Command::new(ROJO_PATH)
.args(&[ .args([
"serve", "serve",
project_path.to_str().unwrap(), project_path.to_str().unwrap(),
"--port", "--port",
@@ -145,14 +145,14 @@ impl TestServeSession {
pub fn get_api_rojo(&self) -> Result<ServerInfoResponse, reqwest::Error> { pub fn get_api_rojo(&self) -> Result<ServerInfoResponse, reqwest::Error> {
let url = format!("http://localhost:{}/api/rojo", self.port); let url = format!("http://localhost:{}/api/rojo", self.port);
let body = reqwest::blocking::get(&url)?.text()?; let body = reqwest::blocking::get(url)?.text()?;
Ok(serde_json::from_str(&body).expect("Server returned malformed response")) Ok(serde_json::from_str(&body).expect("Server returned malformed response"))
} }
pub fn get_api_read(&self, id: Ref) -> Result<ReadResponse, reqwest::Error> { pub fn get_api_read(&self, id: Ref) -> Result<ReadResponse, reqwest::Error> {
let url = format!("http://localhost:{}/api/read/{}", self.port, id); let url = format!("http://localhost:{}/api/read/{}", self.port, id);
let body = reqwest::blocking::get(&url)?.text()?; let body = reqwest::blocking::get(url)?.text()?;
Ok(serde_json::from_str(&body).expect("Server returned malformed response")) Ok(serde_json::from_str(&body).expect("Server returned malformed response"))
} }
@@ -163,7 +163,7 @@ impl TestServeSession {
) -> Result<SubscribeResponse<'static>, reqwest::Error> { ) -> Result<SubscribeResponse<'static>, reqwest::Error> {
let url = format!("http://localhost:{}/api/subscribe/{}", self.port, cursor); let url = format!("http://localhost:{}/api/subscribe/{}", self.port, cursor);
reqwest::blocking::get(&url)?.json() reqwest::blocking::get(url)?.json()
} }
} }

View File

@@ -296,16 +296,27 @@ fn no_name_top_level_project() {
run_serve_test("no_name_top_level_project", |session, mut redactions| { run_serve_test("no_name_top_level_project", |session, mut redactions| {
let info = session.get_api_rojo().unwrap(); let info = session.get_api_rojo().unwrap();
let root_id = info.root_instance_id; let root_id = info.root_instance_id;
assert_yaml_snapshot!( assert_yaml_snapshot!(
"no_name_top_level_project_info", "no_name_top_level_project_info",
redactions.redacted_yaml(info) redactions.redacted_yaml(info)
); );
let read_response = session.get_api_read(root_id).unwrap(); let read_response = session.get_api_read(root_id).unwrap();
assert_yaml_snapshot!( assert_yaml_snapshot!(
"no_name_top_level_project_all", "no_name_top_level_project_all",
read_response.intern_and_redact(&mut redactions, root_id) read_response.intern_and_redact(&mut redactions, root_id)
); );
let project_path = session.path().join("default.project.json");
let mut project_contents = fs::read_to_string(&project_path).unwrap();
project_contents.push('\n');
fs::write(&project_path, project_contents).unwrap();
// The cursor shouldn't be changing so this snapshot is fine for testing
// the response.
let read_response = session.get_api_read(root_id).unwrap();
assert_yaml_snapshot!(
"no_name_top_level_project_all-2",
read_response.intern_and_redact(&mut redactions, root_id)
);
}); });
} }