Skip to content

Commit ea70d89

Browse files
authored
Support .jsonc extension for all JSON files (#1159)
1 parent 03410ce commit ea70d89

File tree

46 files changed

+759
-153
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+759
-153
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ Making a new release? Simply add the new header with the version and date undern
3131

3232
## Unreleased
3333

34+
* Added support for `.jsonc` files for all JSON-related files (e.g. `.project.jsonc` and `.meta.jsonc`) to accompany JSONC support ([#1159])
35+
36+
[#1159]: https://github.com/rojo-rbx/rojo/pull/1159
37+
3438
## [7.6.1] (November 6th, 2025)
3539

3640
* Fixed a bug where the last sync timestamp was not updating correctly in the plugin ([#1132])

src/project.rs

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ use thiserror::Error;
1313

1414
use crate::{glob::Glob, json, resolution::UnresolvedValue, snapshot::SyncRule};
1515

16-
static PROJECT_FILENAME: &str = "default.project.json";
16+
/// Represents 'default' project names that act as `init` files
17+
pub static DEFAULT_PROJECT_NAMES: [&str; 2] = ["default.project.json", "default.project.jsonc"];
1718

1819
/// Error type returned by any function that handles projects.
1920
#[derive(Debug, Error)]
@@ -131,7 +132,7 @@ impl Project {
131132
pub fn is_project_file(path: &Path) -> bool {
132133
path.file_name()
133134
.and_then(|name| name.to_str())
134-
.map(|name| name.ends_with(".project.json"))
135+
.map(|name| name.ends_with(".project.json") || name.ends_with(".project.jsonc"))
135136
.unwrap_or(false)
136137
}
137138

@@ -149,18 +150,19 @@ impl Project {
149150
None
150151
}
151152
} else {
152-
let child_path = path.join(PROJECT_FILENAME);
153-
let child_meta = fs::metadata(&child_path).ok()?;
153+
for filename in DEFAULT_PROJECT_NAMES {
154+
let child_path = path.join(filename);
155+
let child_meta = fs::metadata(&child_path).ok()?;
154156

155-
if child_meta.is_file() {
156-
Some(child_path)
157-
} else {
158-
// This is a folder with the same name as a Rojo default project
159-
// file.
160-
//
161-
// That's pretty weird, but we can roll with it.
162-
None
157+
if child_meta.is_file() {
158+
return Some(child_path);
159+
}
163160
}
161+
// This is a folder with the same name as a Rojo default project
162+
// file.
163+
//
164+
// That's pretty weird, but we can roll with it.
165+
None
164166
}
165167
}
166168

@@ -181,16 +183,20 @@ impl Project {
181183

182184
// If you're editing this to be generic, make sure you also alter the
183185
// snapshot middleware to support generic init paths.
184-
if file_name == PROJECT_FILENAME {
185-
let folder_name = self.folder_location().file_name().and_then(OsStr::to_str);
186-
if let Some(folder_name) = folder_name {
187-
self.name = Some(folder_name.to_string());
188-
} else {
189-
return Err(Error::FolderNameInvalid {
190-
path: self.file_location.clone(),
191-
});
186+
for default_file_name in DEFAULT_PROJECT_NAMES {
187+
if file_name == default_file_name {
188+
let folder_name = self.folder_location().file_name().and_then(OsStr::to_str);
189+
if let Some(folder_name) = folder_name {
190+
self.name = Some(folder_name.to_string());
191+
return Ok(());
192+
} else {
193+
return Err(Error::FolderNameInvalid {
194+
path: self.file_location.clone(),
195+
});
196+
}
192197
}
193-
} else if let Some(fallback) = fallback {
198+
}
199+
if let Some(fallback) = fallback {
194200
self.name = Some(fallback.to_string());
195201
} else {
196202
// As of the time of writing (July 10, 2024) there is no way for
@@ -257,6 +263,10 @@ impl Project {
257263
project_file_location: &Path,
258264
fallback_name: Option<&str>,
259265
) -> Result<Self, ProjectError> {
266+
log::debug!(
267+
"Loading project file from {}",
268+
project_file_location.display()
269+
);
260270
let project_path = project_file_location.to_path_buf();
261271
let contents = vfs.read(&project_path).map_err(|e| match e.kind() {
262272
io::ErrorKind::NotFound => Error::NoProjectFound {
@@ -272,6 +282,24 @@ impl Project {
272282
)?)
273283
}
274284

285+
pub(crate) fn load_initial_project(vfs: &Vfs, path: &Path) -> Result<Self, ProjectError> {
286+
if Self::is_project_file(path) {
287+
Self::load_exact(vfs, path, None)
288+
} else {
289+
// Check for default projects.
290+
for default_project_name in DEFAULT_PROJECT_NAMES {
291+
let project_path = path.join(default_project_name);
292+
if project_path.exists() {
293+
return Self::load_exact(vfs, &project_path, None);
294+
}
295+
}
296+
Err(Error::NoProjectFound {
297+
path: path.to_path_buf(),
298+
}
299+
.into())
300+
}
301+
}
302+
275303
/// Checks if there are any compatibility issues with this project file and
276304
/// warns the user if there are any.
277305
fn check_compatibility(&self) {
@@ -530,7 +558,7 @@ mod test {
530558

531559
let project = Project::load_from_slice(
532560
project_json.as_bytes(),
533-
PathBuf::from("/test/default.project.json"),
561+
PathBuf::from("/test/default.project.jsonc"),
534562
None,
535563
)
536564
.expect("Failed to parse project with JSONC features");

src/serve_session.rs

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use std::{
2-
borrow::Cow,
32
collections::HashSet,
43
io,
54
net::IpAddr,
@@ -101,15 +100,7 @@ impl ServeSession {
101100

102101
log::trace!("Starting new ServeSession at path {}", start_path.display());
103102

104-
let project_path = if Project::is_project_file(start_path) {
105-
Cow::Borrowed(start_path)
106-
} else {
107-
Cow::Owned(start_path.join("default.project.json"))
108-
};
109-
110-
log::debug!("Loading project file from {}", project_path.display());
111-
112-
let root_project = Project::load_exact(&vfs, &project_path, None)?;
103+
let root_project = Project::load_initial_project(&vfs, start_path)?;
113104

114105
let mut tree = RojoTree::new(InstanceSnapshot::new());
115106

src/snapshot_middleware/csv.rs

Lines changed: 77 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
use std::{collections::BTreeMap, path::Path};
22

33
use anyhow::Context;
4-
use memofs::{IoResultExt, Vfs};
4+
use memofs::Vfs;
55
use rbx_dom_weak::ustr;
66
use serde::Serialize;
77

8-
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
9-
10-
use super::{
11-
dir::{dir_meta, snapshot_dir_no_meta},
12-
meta_file::AdjacentMetadata,
8+
use crate::{
9+
snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot},
10+
snapshot_middleware::meta_file::DirectoryMetadata,
1311
};
1412

13+
use super::{dir::snapshot_dir_no_meta, meta_file::AdjacentMetadata};
14+
1515
pub fn snapshot_csv(
1616
_context: &InstanceContext,
1717
vfs: &Vfs,
1818
path: &Path,
1919
name: &str,
2020
) -> anyhow::Result<Option<InstanceSnapshot>> {
21-
let meta_path = path.with_file_name(format!("{}.meta.json", name));
2221
let contents = vfs.read(path)?;
2322

2423
let table_contents = convert_localization_csv(&contents).with_context(|| {
@@ -35,13 +34,10 @@ pub fn snapshot_csv(
3534
.metadata(
3635
InstanceMetadata::new()
3736
.instigating_source(path)
38-
.relevant_paths(vec![path.to_path_buf(), meta_path.clone()]),
37+
.relevant_paths(vec![path.to_path_buf()]),
3938
);
4039

41-
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
42-
let mut metadata = AdjacentMetadata::from_slice(&meta_contents, meta_path)?;
43-
metadata.apply_all(&mut snapshot)?;
44-
}
40+
AdjacentMetadata::read_and_apply_all(vfs, path, name, &mut snapshot)?;
4541

4642
Ok(Some(snapshot))
4743
}
@@ -75,9 +71,7 @@ pub fn snapshot_csv_init(
7571
init_snapshot.children = dir_snapshot.children;
7672
init_snapshot.metadata = dir_snapshot.metadata;
7773

78-
if let Some(mut meta) = dir_meta(vfs, folder_path)? {
79-
meta.apply_all(&mut init_snapshot)?;
80-
}
74+
DirectoryMetadata::read_and_apply_all(vfs, folder_path, &mut init_snapshot)?;
8175

8276
Ok(Some(init_snapshot))
8377
}
@@ -223,4 +217,72 @@ Ack,Ack!,,An exclamation of despair,¡Ay!"#,
223217

224218
insta::assert_yaml_snapshot!(instance_snapshot);
225219
}
220+
221+
#[test]
222+
fn csv_init() {
223+
let mut imfs = InMemoryFs::new();
224+
imfs.load_snapshot(
225+
"/root",
226+
VfsSnapshot::dir([(
227+
"init.csv",
228+
VfsSnapshot::file(
229+
r#"
230+
Key,Source,Context,Example,es
231+
Ack,Ack!,,An exclamation of despair,¡Ay!"#,
232+
),
233+
)]),
234+
)
235+
.unwrap();
236+
237+
let vfs = Vfs::new(imfs);
238+
239+
let instance_snapshot = snapshot_csv_init(
240+
&InstanceContext::with_emit_legacy_scripts(Some(true)),
241+
&vfs,
242+
Path::new("/root/init.csv"),
243+
)
244+
.unwrap()
245+
.unwrap();
246+
247+
insta::with_settings!({ sort_maps => true }, {
248+
insta::assert_yaml_snapshot!(instance_snapshot);
249+
});
250+
}
251+
252+
#[test]
253+
fn csv_init_with_meta() {
254+
let mut imfs = InMemoryFs::new();
255+
imfs.load_snapshot(
256+
"/root",
257+
VfsSnapshot::dir([
258+
(
259+
"init.csv",
260+
VfsSnapshot::file(
261+
r#"
262+
Key,Source,Context,Example,es
263+
Ack,Ack!,,An exclamation of despair,¡Ay!"#,
264+
),
265+
),
266+
(
267+
"init.meta.json",
268+
VfsSnapshot::file(r#"{"id": "manually specified"}"#),
269+
),
270+
]),
271+
)
272+
.unwrap();
273+
274+
let vfs = Vfs::new(imfs);
275+
276+
let instance_snapshot = snapshot_csv_init(
277+
&InstanceContext::with_emit_legacy_scripts(Some(true)),
278+
&vfs,
279+
Path::new("/root/init.csv"),
280+
)
281+
.unwrap()
282+
.unwrap();
283+
284+
insta::with_settings!({ sort_maps => true }, {
285+
insta::assert_yaml_snapshot!(instance_snapshot);
286+
});
287+
}
226288
}

src/snapshot_middleware/dir.rs

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::path::Path;
22

3-
use memofs::{DirEntry, IoResultExt, Vfs};
3+
use memofs::{DirEntry, Vfs};
44

55
use crate::snapshot::{InstanceContext, InstanceMetadata, InstanceSnapshot};
66

@@ -16,26 +16,11 @@ pub fn snapshot_dir(
1616
None => return Ok(None),
1717
};
1818

19-
if let Some(mut meta) = dir_meta(vfs, path)? {
20-
meta.apply_all(&mut snapshot)?;
21-
}
19+
DirectoryMetadata::read_and_apply_all(vfs, path, &mut snapshot)?;
2220

2321
Ok(Some(snapshot))
2422
}
2523

26-
/// Retrieves the meta file that should be applied for this directory, if it
27-
/// exists.
28-
pub fn dir_meta(vfs: &Vfs, path: &Path) -> anyhow::Result<Option<DirectoryMetadata>> {
29-
let meta_path = path.join("init.meta.json");
30-
31-
if let Some(meta_contents) = vfs.read(&meta_path).with_not_found()? {
32-
let metadata = DirectoryMetadata::from_slice(&meta_contents, meta_path)?;
33-
Ok(Some(metadata))
34-
} else {
35-
Ok(None)
36-
}
37-
}
38-
3924
/// Snapshot a directory without applying meta files; useful for if the
4025
/// directory's ClassName will change before metadata should be applied. For
4126
/// example, this can happen if the directory contains an `init.client.lua`
@@ -73,11 +58,8 @@ pub fn snapshot_dir_no_meta(
7358
.ok_or_else(|| anyhow::anyhow!("File name was not valid UTF-8: {}", path.display()))?
7459
.to_string();
7560

76-
let meta_path = path.join("init.meta.json");
77-
7861
let relevant_paths = vec![
7962
path.to_path_buf(),
80-
meta_path,
8163
// TODO: We shouldn't need to know about Lua existing in this
8264
// middleware. Should we figure out a way for that function to add
8365
// relevant paths to this middleware?

0 commit comments

Comments
 (0)