Add seamless support for using JSON5 in the config file (#47)

Co-authored-by: Lucas Nogueira <lucas@tauri.studio>
This commit is contained in:
Lucas Nogueira 2022-02-03 10:15:32 -03:00
parent 61851f49ea
commit 995de57a76
No known key found for this signature in database
GPG Key ID: 2714B66BCFB01F7F
15 changed files with 320 additions and 72 deletions

21
.changes/json5.md Normal file
View File

@ -0,0 +1,21 @@
---
"tauri": patch
"tauri-build": patch
"tauri-codegen": patch
"tauri-macros": patch
"tauri-utils": patch
"cli.rs": patch
---
Adds support for using JSON5 format for the `tauri.conf.json` file, along with also supporting the `.json5` extension.
Here is the logic flow that determines if JSON or JSON5 will be used to parse the config:
1. Check if `tauri.conf.json` exists
a. Parse it with `serde_json`
b. Parse it with `json5` if `serde_json` fails
c. Return original `serde_json` error if all above steps failed
2. Check if `tauri.conf.json5` exists
a. Parse it with `json5`
b. Return error if all above steps failed
3. Return error if all above steps failed

View File

@ -20,9 +20,9 @@ rustdoc-args = [ "--cfg", "doc_cfg" ]
anyhow = "1"
quote = { version = "1", optional = true }
tauri-codegen = { version = "1.0.0-beta.4", path = "../tauri-codegen", optional = true }
serde_json = "1.0"
tauri-utils = { version = "1.0.0-beta.0", path = "../tauri-utils", features = [ "build" ] }
cargo_toml = "0.10"
serde_json = "1"
[target."cfg(windows)".dependencies]
winres = "0.1"
@ -30,3 +30,4 @@ winres = "0.1"
[features]
codegen = [ "tauri-codegen", "quote" ]
isolation = ["tauri-codegen/isolation", "tauri-utils/isolation"]
config-json5 = [ "tauri-utils/config-json5" ]

View File

@ -120,7 +120,7 @@ impl CodegenContext {
)
})?;
writeln!(&mut file, "{}", code).with_context(|| {
writeln!(file, "{}", code).with_context(|| {
format!(
"Unable to write tokenstream to out file during tauri-build {}",
out.display()

View File

@ -109,13 +109,20 @@ pub fn build() {
pub fn try_build(attributes: Attributes) -> Result<()> {
use anyhow::anyhow;
use cargo_toml::{Dependency, Manifest};
use std::fs::read_to_string;
use tauri_utils::config::Config;
println!("cargo:rerun-if-changed=tauri.conf.json");
println!("cargo:rerun-if-changed=src/Cargo.toml");
println!("cargo:rerun-if-changed=tauri.conf.json");
#[cfg(feature = "config-json5")]
println!("cargo:rerun-if-changed=tauri.conf.json5");
let config: Config = serde_json::from_str(&read_to_string("tauri.conf.json")?)?;
let config: Config = if let Ok(env) = std::env::var("TAURI_CONFIG") {
serde_json::from_str(&env)?
} else {
serde_json::from_value(tauri_utils::config::parse::read_from(
std::env::current_dir().unwrap(),
)?)?
};
let mut manifest = Manifest::from_path("Cargo.toml")?;
if let Some(tauri) = manifest.dependencies.remove("tauri") {

View File

@ -32,3 +32,4 @@ default = [ "compression" ]
compression = [ "zstd", "tauri-utils/compression" ]
isolation = ["tauri-utils/isolation"]
shell-scope = []
config-json5 = [ "tauri-utils/config-json5" ]

View File

@ -2,23 +2,20 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
pub use context::{context_codegen, ContextData};
pub use self::context::{context_codegen, ContextData};
use std::{
borrow::Cow,
fs::File,
io::BufReader,
path::{Path, PathBuf},
};
use tauri_utils::config::Config;
use thiserror::Error;
pub use tauri_utils::config::{parse::ConfigError, Config};
mod context;
pub mod embedded_assets;
/// Represents all the errors that can happen while reading the config.
#[derive(Debug, Error)]
/// Represents all the errors that can happen while reading the config during codegen.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ConfigError {
pub enum CodegenConfigError {
#[error("unable to access current working directory: {0}")]
CurrentDir(std::io::Error),
@ -26,29 +23,20 @@ pub enum ConfigError {
#[error("Tauri config file has no parent, this shouldn't be possible. file an issue on https://github.com/tauri-apps/tauri - target {0}")]
Parent(PathBuf),
#[error("unable to parse inline TAURI_CONFIG env var: {0}")]
#[error("unable to parse inline JSON TAURI_CONFIG env var: {0}")]
FormatInline(serde_json::Error),
#[error("unable to parse Tauri config file at {path} because {error}")]
Format {
path: PathBuf,
error: serde_json::Error,
},
#[error("unable to read Tauri config file at {path} because {error}")]
Io {
path: PathBuf,
error: std::io::Error,
},
#[error("{0}")]
ConfigError(#[from] ConfigError),
}
/// Get the [`Config`] from the `TAURI_CONFIG` environmental variable, or read from the passed path.
///
/// If the passed path is relative, it should be relative to the current working directory of the
/// compiling crate.
pub fn get_config(path: &Path) -> Result<(Config, PathBuf), ConfigError> {
pub fn get_config(path: &Path) -> Result<(Config, PathBuf), CodegenConfigError> {
let path = if path.is_relative() {
let cwd = std::env::current_dir().map_err(ConfigError::CurrentDir)?;
let cwd = std::env::current_dir().map_err(CodegenConfigError::CurrentDir)?;
Cow::Owned(cwd.join(path))
} else {
Cow::Borrowed(path)
@ -59,27 +47,16 @@ pub fn get_config(path: &Path) -> Result<(Config, PathBuf), ConfigError> {
// already unlikely unless the developer goes out of their way to run the cli on a different
// project than the target crate.
let config = if let Ok(env) = std::env::var("TAURI_CONFIG") {
serde_json::from_str(&env).map_err(ConfigError::FormatInline)?
serde_json::from_str(&env).map_err(CodegenConfigError::FormatInline)?
} else {
File::open(&path)
.map_err(|error| ConfigError::Io {
path: path.clone().into_owned(),
error,
})
.map(BufReader::new)
.and_then(|file| {
serde_json::from_reader(file).map_err(|error| ConfigError::Format {
path: path.clone().into_owned(),
error,
})
})?
tauri_utils::config::parse(path.to_path_buf())?
};
// this should be impossible because of the use of `current_dir()` above, but handle it anyways
let parent = path
.parent()
.map(ToOwned::to_owned)
.ok_or_else(|| ConfigError::Parent(path.into_owned()))?;
.ok_or_else(|| CodegenConfigError::Parent(path.into_owned()))?;
Ok((config, parent))
}

View File

@ -21,9 +21,11 @@ quote = "1"
syn = { version = "1", features = [ "full" ] }
heck = "0.3"
tauri-codegen = { version = "1.0.0-beta.4", default-features = false, path = "../tauri-codegen" }
tauri-utils = { version = "1.0.0-beta.3", path = "../tauri-utils" }
[features]
custom-protocol = [ ]
compression = [ "tauri-codegen/compression" ]
isolation = ["tauri-codegen/isolation"]
shell-scope = ["tauri-codegen/shell-scope"]
config-json5 = [ "tauri-codegen/config-json5", "tauri-utils/config-json5" ]

View File

@ -11,6 +11,7 @@ use syn::{
LitStr, PathArguments, PathSegment, Token,
};
use tauri_codegen::{context_codegen, get_config, ContextData};
use tauri_utils::config::parse::does_supported_extension_exist;
pub(crate) struct ContextItems {
config_file: PathBuf,
@ -35,7 +36,7 @@ impl Parse for ContextItems {
VarError::NotUnicode(_) => "CARGO_MANIFEST_DIR env var contained invalid utf8".into(),
})
.and_then(|path| {
if path.exists() {
if does_supported_extension_exist(&path) {
Ok(path)
} else {
Err(format!(

View File

@ -31,6 +31,8 @@ ring = { version = "0.16", optional = true, features = ["std"] }
once_cell = { version = "1.8", optional = true }
serialize-to-javascript = { git = "https://github.com/chippers/serialize-to-javascript" }
ctor = "0.1"
json5 = { version = "0.4", optional = true }
json-patch = "0.2"
[target."cfg(target_os = \"linux\")".dependencies]
heck = "0.4"
@ -41,3 +43,4 @@ compression = [ "zstd" ]
schema = ["schemars"]
isolation = [ "aes-gcm", "ring", "once_cell" ]
process-relaunch-dangerous-allow-symlink-macos = []
config-json5 = [ "json5" ]

View File

@ -24,6 +24,11 @@ use url::Url;
use std::{collections::HashMap, fmt, fs::read_to_string, path::PathBuf};
/// Items to help with parsing content into a [`Config`].
pub mod parse;
pub use self::parse::parse;
/// The window webview URL options.
#[derive(PartialEq, Debug, Clone, Deserialize, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]

View File

@ -0,0 +1,236 @@
// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::config::Config;
use json_patch::merge;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use thiserror::Error;
/// All extensions that are possibly supported, but perhaps not enabled.
pub const EXTENSIONS_SUPPORTED: &[&str] = &["json", "json5"];
/// All extensions that are currently enabled.
pub const EXTENSIONS_ENABLED: &[&str] = &[
"json",
#[cfg(feature = "config-json5")]
"json5",
];
/// Represents all the errors that can happen while reading the config.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ConfigError {
/// Failed to parse from JSON.
#[error("unable to parse JSON Tauri config file at {path} because {error}")]
FormatJson {
/// The path that failed to parse into JSON.
path: PathBuf,
/// The parsing [`serde_json::Error`].
error: serde_json::Error,
},
/// Failed to parse from JSON5.
#[cfg(feature = "config-json5")]
#[error("unable to parse JSON5 Tauri config file at {path} because {error}")]
FormatJson5 {
/// The path that failed to parse into JSON5.
path: PathBuf,
/// The parsing [`json5::Error`].
error: ::json5::Error,
},
/// Unknown file extension encountered.
#[error("unsupported format encountered {0}")]
UnsupportedFormat(String),
/// Known file extension encountered, but corresponding parser is not enabled (cargo features).
#[error("supported (but disabled) format encountered {extension} - try enabling `{feature}` ")]
DisabledFormat {
/// The extension encountered.
extension: String,
/// The cargo feature to enable it.
feature: String,
},
/// A generic IO error with context of what caused it.
#[error("unable to read Tauri config file at {path} because {error}")]
Io {
/// The path the IO error occured on.
path: PathBuf,
/// The [`std::io::Error`].
error: std::io::Error,
},
}
/// Reads the configuration from the given root directory.
///
/// It first looks for a `tauri.conf.json[5]` file on the given directory. The file must exist.
/// Then it looks for a platform-specific configuration file:
/// - `tauri.macos.conf.json[5]` on macOS
/// - `tauri.linux.conf.json[5]` on Linux
/// - `tauri.windows.conf.json[5]` on Windows
/// Merging the configurations using [JSON Merge Patch (RFC 7396)].
///
/// [JSON Merge Patch (RFC 7396)]: https://datatracker.ietf.org/doc/html/rfc7396.
pub fn read_from(root_dir: PathBuf) -> Result<Value, ConfigError> {
let mut config: Value = parse_value(root_dir.join("tauri.conf.json"))?;
let platform_config_filename = if cfg!(target_os = "macos") {
"tauri.macos.conf.json"
} else if cfg!(windows) {
"tauri.windows.conf.json"
} else {
"tauri.linux.conf.json"
};
let platform_config_path = root_dir.join(platform_config_filename);
if does_supported_extension_exist(&platform_config_path) {
let platform_config: Value = parse_value(platform_config_path)?;
merge(&mut config, &platform_config);
}
Ok(config)
}
/// Check if a supported config file exists at path.
///
/// The passed path is expected to be the path to the "default" configuration format, in this case
/// JSON with `.json`.
pub fn does_supported_extension_exist(path: impl Into<PathBuf>) -> bool {
let path = path.into();
EXTENSIONS_ENABLED
.iter()
.any(|ext| path.with_extension(ext).exists())
}
/// Parse the config from path, including alternative formats.
///
/// Hierarchy:
/// 1. Check if `tauri.conf.json` exists
/// a. Parse it with `serde_json`
/// b. Parse it with `json5` if `serde_json` fails
/// c. Return original `serde_json` error if all above steps failed
/// 2. Check if `tauri.conf.json5` exists
/// a. Parse it with `json5`
/// b. Return error if all above steps failed
/// 3. Return error if all above steps failed
pub fn parse(path: impl Into<PathBuf>) -> Result<Config, ConfigError> {
do_parse(path.into())
}
/// See [`parse`] for specifics, returns a JSON [`Value`] instead of [`Config`].
pub fn parse_value(path: impl Into<PathBuf>) -> Result<Value, ConfigError> {
do_parse(path.into())
}
fn do_parse<D: DeserializeOwned>(path: PathBuf) -> Result<D, ConfigError> {
let json5 = path.with_extension("json5");
let path_ext = path
.extension()
.map(OsStr::to_string_lossy)
.unwrap_or_default();
if path.exists() {
let raw = read_to_string(&path)?;
// to allow us to easily use the compile-time #[cfg], we always bind
#[allow(clippy::let_and_return)]
let json = do_parse_json(&raw, &path);
// we also want to support **valid** json5 in the .json extension if the feature is enabled.
// if the json5 is not valid the serde_json error for regular json will be returned.
// this could be a bit confusing, so we may want to encourage users using json5 to use the
// .json5 extension instead of .json
#[cfg(feature = "config-json5")]
let json = {
match do_parse_json5(&raw, &path) {
json5 @ Ok(_) => json5,
// assume any errors from json5 in a .json file is because it's not json5
Err(_) => json,
}
};
json
} else if json5.exists() {
#[cfg(feature = "config-json5")]
{
let raw = read_to_string(&json5)?;
do_parse_json5(&raw, &path)
}
#[cfg(not(feature = "config-json5"))]
Err(ConfigError::DisabledFormat {
extension: ".json5".into(),
feature: "config-json5".into(),
})
} else if !EXTENSIONS_SUPPORTED.contains(&path_ext.as_ref()) {
Err(ConfigError::UnsupportedFormat(path_ext.to_string()))
} else {
Err(ConfigError::Io {
path,
error: std::io::ErrorKind::NotFound.into(),
})
}
}
/// "Low-level" helper to parse JSON into a [`Config`].
///
/// `raw` should be the contents of the file that is represented by `path`.
pub fn parse_json(raw: &str, path: &Path) -> Result<Config, ConfigError> {
do_parse_json(raw, path)
}
/// "Low-level" helper to parse JSON into a JSON [`Value`].
///
/// `raw` should be the contents of the file that is represented by `path`.
pub fn parse_json_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
do_parse_json(raw, path)
}
fn do_parse_json<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
serde_json::from_str(raw).map_err(|error| ConfigError::FormatJson {
path: path.into(),
error,
})
}
/// "Low-level" helper to parse JSON5 into a [`Config`].
///
/// `raw` should be the contents of the file that is represented by `path`. This function requires
/// the `config-json5` feature to be enabled.
#[cfg(feature = "config-json5")]
pub fn parse_json5(raw: &str, path: &Path) -> Result<Config, ConfigError> {
do_parse_json5(raw, path)
}
/// "Low-level" helper to parse JSON5 into a JSON [`Value`].
///
/// `raw` should be the contents of the file that is represented by `path`. This function requires
/// the `config-json5` feature to be enabled.
#[cfg(feature = "config-json5")]
pub fn parse_json5_value(raw: &str, path: &Path) -> Result<Value, ConfigError> {
do_parse_json5(raw, path)
}
#[cfg(feature = "config-json5")]
fn do_parse_json5<D: DeserializeOwned>(raw: &str, path: &Path) -> Result<D, ConfigError> {
::json5::from_str(raw).map_err(|error| ConfigError::FormatJson5 {
path: path.into(),
error,
})
}
/// Helper function to wrap IO errors from [`std::fs::read_to_string`] into a [`ConfigError`].
fn read_to_string(path: &Path) -> Result<String, ConfigError> {
std::fs::read_to_string(path).map_err(|error| ConfigError::Io {
path: path.into(),
error,
})
}

View File

@ -232,6 +232,9 @@ window-start-dragging = [ ]
window-print = [ ]
egui = ["epi", "tauri-runtime-wry/egui"]
# features unrelated to api/endpoints
config-json5 = [ "tauri-macros/config-json5" ]
[[example]]
name = "commands"
path = "../../examples/commands/src-tauri/src/main.rs"

View File

@ -1071,6 +1071,17 @@ dependencies = [
"serde_json",
]
[[package]]
name = "json5"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
dependencies = [
"pest",
"pest_derive",
"serde",
]
[[package]]
name = "jsonway"
version = "2.0.0"
@ -2467,6 +2478,8 @@ dependencies = [
"ctor",
"heck",
"html5ever",
"json-patch",
"json5",
"kuchiki",
"once_cell",
"phf 0.10.1",

View File

@ -30,8 +30,7 @@ notify = "4.0"
shared_child = "1.0"
toml_edit = "0.12"
json-patch = "0.2"
tauri-utils = { version = "1.0.0-beta.3", path = "../../core/tauri-utils", features = ["isolation", "schema"] }
schemars = { version = "0.8", features = ["url"] }
tauri-utils = { version = "1.0.0-beta.3", path = "../../core/tauri-utils", features = ["isolation", "schema", "config-json5"] }
toml = "0.5"
valico = "3.6"
handlebars = "4.2"

View File

@ -29,8 +29,6 @@ pub fn wix_settings(config: WixConfig) -> tauri_bundler::WixSettings {
use std::{
env::set_var,
fs::File,
io::BufReader,
process::exit,
sync::{Arc, Mutex},
};
@ -48,11 +46,13 @@ fn get_internal(merge_config: Option<&str>, reload: bool) -> crate::Result<Confi
return Ok(config_handle().clone());
}
let path = super::app_paths::tauri_dir().join("tauri.conf.json");
let file = File::open(path)?;
let buf = BufReader::new(file);
let mut config: JsonValue =
serde_json::from_reader(buf).with_context(|| "failed to parse `tauri.conf.json`")?;
let mut config = tauri_utils::config::parse::read_from(super::app_paths::tauri_dir())?;
if let Some(merge_config) = merge_config {
let merge_config: JsonValue =
serde_json::from_str(merge_config).with_context(|| "failed to parse config to merge")?;
merge(&mut config, &merge_config);
}
let schema: JsonValue = serde_json::from_str(include_str!("../../schema.json"))?;
let mut scope = valico::json_schema::Scope::new();
@ -74,27 +74,6 @@ fn get_internal(merge_config: Option<&str>, reload: bool) -> crate::Result<Confi
exit(1);
}
if let Some(merge_config) = merge_config {
let merge_config: JsonValue =
serde_json::from_str(merge_config).with_context(|| "failed to parse config to merge")?;
merge(&mut config, &merge_config);
}
let platform_config_filename = if cfg!(target_os = "macos") {
"tauri.macos.conf.json"
} else if cfg!(windows) {
"tauri.windows.conf.json"
} else {
"tauri.linux.conf.json"
};
let platform_config_path = super::app_paths::tauri_dir().join(platform_config_filename);
if platform_config_path.exists() {
let platform_config_file = File::open(platform_config_path)?;
let platform_config: JsonValue = serde_json::from_reader(BufReader::new(platform_config_file))
.with_context(|| format!("failed to parse `{}`", platform_config_filename))?;
merge(&mut config, &platform_config);
}
let config: Config = serde_json::from_value(config)?;
set_var("TAURI_CONFIG", serde_json::to_string(&config)?);
*config_handle().lock().unwrap() = Some(config);