From 961d55e54f878236d41b87b96833e63f32c7743a Mon Sep 17 00:00:00 2001 From: Robbie McKinstry Date: Thu, 6 Nov 2025 16:16:01 -0500 Subject: [PATCH] Add support for JSONC Wrangler configuration files. --- Cargo.lock | 61 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/fs/mod.rs | 30 ++++++++++++++----- src/fs/wrangler.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 159 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7dfd73d..1a9a6df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2346,6 +2346,7 @@ dependencies = [ "reqwest 0.12.24", "serde", "serde_json", + "serde_json5", "serde_with", "static_assertions", "tempfile", @@ -2601,6 +2602,49 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pest" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.108", +] + +[[package]] +name = "pest_meta" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" +dependencies = [ + "pest", + "sha2", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -3667,6 +3711,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_json5" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d34d03f54462862f2a42918391c9526337f53171eaa4d8894562be7f252edd3" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -4478,6 +4533,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "uname" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 512a996..8e3f993 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ rand = { version = "0.9.0", features = ["small_rng"] } reqwest = "0.12.15" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +serde_json5 = "0.2.1" serde_with = { version = "3.12", features = ["chrono"] } thiserror.workspace = true tokio = { workspace = true, features = ["full"] } diff --git a/src/fs/mod.rs b/src/fs/mod.rs index c62e889..f500d5c 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -12,7 +12,7 @@ use std::{ pub(crate) use file::File; pub(crate) use manifest::application_manifest; pub(crate) use session::{Session, SessionFile, UserCreds}; -pub(crate) use wrangler::{JsonWranglerFile, TomlWranglerFile}; +pub(crate) use wrangler::{JsonWranglerFile, JsoncWranglerFile, TomlWranglerFile}; use manifest::{JsonManifest, Manifest, TomlManifest}; use wrangler::Wrangler; @@ -77,15 +77,17 @@ impl FileSystem { Ok(manifest_box) } - /// Load the wrangler configuration file, looking for both TOML and JSON formats + /// Load the wrangler configuration file, looking for TOML, JSON, and JSONC formats pub fn wrangler_config(&self) -> Result { - // Attempt to load a TOML wrangler file. Fallback to JSON. + // Attempt to load a TOML wrangler file. Fallback to JSON, then JSONC. let toml_wrangler = self.load_file(TomlWranglerFile); let json_wrangler = self.load_file(JsonWranglerFile); - let wrangler_config = match (toml_wrangler, json_wrangler) { - (Ok(wrangler), _) => wrangler, - (Err(_), Ok(wrangler)) => wrangler, - (Err(_), Err(_)) => return Err(WranglerMissing), + let jsonc_wrangler = self.load_file(JsoncWranglerFile); + let wrangler_config = match (toml_wrangler, json_wrangler, jsonc_wrangler) { + (Ok(wrangler), _, _) => wrangler, + (Err(_), Ok(wrangler), _) => wrangler, + (Err(_), Err(_), Ok(wrangler)) => wrangler, + (Err(_), Err(_), Err(_)) => return Err(WranglerMissing), }; Ok(wrangler_config) } @@ -119,6 +121,7 @@ impl FileSystem { match F::EXTENSION { "toml" => self.read_toml_file(file), "json" => self.read_json_file(file), + "jsonc" => self.read_jsonc_file(file), _ => Err(miette!( "Extension unknown! Internal error. Please file this error as a bug." )), @@ -150,6 +153,18 @@ impl FileSystem { Ok(document) } + /// Open the file and deserialize it with serde (JSONC with comments support). + fn read_jsonc_file(&self, file: F) -> Result { + // • Get the path to the file. + let path = file.path(self)?; + // • Open it as a byte stream, then deserialize those bytes. + let mut buffer = String::new(); + let mut file = std::fs::File::open(path).into_diagnostic()?; + file.read_to_string(&mut buffer).into_diagnostic()?; + let document = serde_json5::from_str(&buffer).into_diagnostic()?; + Ok(document) + } + /// Store the file, using its canonical path. pub(crate) fn save_file(&self, file: &F, blob: &F::Data) -> Result<()> { // • Get the path to the file. @@ -159,6 +174,7 @@ impl FileSystem { let marshalled = match F::EXTENSION { "toml" => toml::to_string_pretty(blob).into_diagnostic()?, "json" => serde_json::to_string_pretty(blob).into_diagnostic()?, + "jsonc" => serde_json5::to_string(blob).into_diagnostic()?, _ => { return Err(miette!( "Extension unknown! Internal error. Please file this error as a bug." diff --git a/src/fs/wrangler.rs b/src/fs/wrangler.rs index 2a6da91..28317a6 100644 --- a/src/fs/wrangler.rs +++ b/src/fs/wrangler.rs @@ -94,7 +94,7 @@ pub struct Wrangler { } const WRANGLER_PREFIX: &str = "wrangler"; -const WRANGLER_EXTENSIONS: [&str; 2] = ["toml", "json"]; +const WRANGLER_EXTENSIONS: [&str; 3] = ["toml", "json", "jsonc"]; /// A TOML formatted wrangler file pub struct TomlWranglerFile; @@ -116,6 +116,16 @@ impl StaticFile for JsonWranglerFile { const EXTENSION: &'static str = WRANGLER_EXTENSIONS[1]; } +/// A JSONC formatted wrangler file +pub struct JsoncWranglerFile; + +impl StaticFile for JsoncWranglerFile { + type Data = Wrangler; + const DIR: DirectoryType = DirectoryType::ApplicationRoot; + const NAME: &'static str = WRANGLER_PREFIX; + const EXTENSION: &'static str = WRANGLER_EXTENSIONS[2]; +} + #[cfg(test)] mod tests { use super::*; @@ -188,4 +198,67 @@ mod tests { let _deserialized: Wrangler = toml::from_str(&toml_str).expect("Failed to deserialize from TOML"); } + + #[test] + fn test_wrangler_serde_jsonc() { + const RAW_JSONC: &str = r#"/** + * For more details on how to configure Wrangler, refer to: + * https://developers.cloudflare.com/workers/wrangler/configuration/ + */ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "multitool-quickstart", + "account_id": "986dc7f2976d17c6205288a7a946ef6a", + "main": "src/index.js", + "compatibility_date": "2025-11-06", + "observability": { + "enabled": true + } + /** + * Smart Placement + * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement + */ + // "placement": { "mode": "smart" } + /** + * Bindings + * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including + * databases, object storage, AI inference, real-time communication and more. + * https://developers.cloudflare.com/workers/runtime-apis/bindings/ + */ + /** + * Environment Variables + * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables + */ + // "vars": { "MY_VARIABLE": "production_value" } + /** + * Note: Use secrets to store sensitive data. + * https://developers.cloudflare.com/workers/configuration/secrets/ + */ + /** + * Static Assets + * https://developers.cloudflare.com/workers/static-assets/binding/ + */ + // "assets": { "directory": "./public/", "binding": "ASSETS" } + /** + * Service Bindings (communicate between multiple Workers) + * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + */ + // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] +}"#; + + let observed: Wrangler = serde_json5::from_str(RAW_JSONC).expect("Failed to parse JSONC"); + + assert_eq!(observed.name, "multitool-quickstart"); + assert_eq!( + observed.account_id, + Some("986dc7f2976d17c6205288a7a946ef6a".to_string()) + ); + assert_eq!(observed.main, "src/index.js"); + assert_eq!(observed.compatibility_date, Some("2025-11-06".to_string())); + assert!(observed.observability.is_some()); + if let Some(observability) = observed.observability { + assert!(observability.enabled); + assert_eq!(observability.head_sampling_rate, None); + } + } }