diff options
Diffstat (limited to 'cli/config_file.rs')
-rw-r--r-- | cli/config_file.rs | 426 |
1 files changed, 426 insertions, 0 deletions
diff --git a/cli/config_file.rs b/cli/config_file.rs new file mode 100644 index 000000000..fcc702ad5 --- /dev/null +++ b/cli/config_file.rs @@ -0,0 +1,426 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use crate::fs_util::canonicalize_path; +use deno_core::error::AnyError; +use deno_core::error::Context; +use deno_core::serde::Deserialize; +use deno_core::serde::Serialize; +use deno_core::serde::Serializer; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::Value; +use std::collections::BTreeMap; +use std::collections::HashMap; +use std::fmt; +use std::path::Path; +use std::path::PathBuf; + +/// The transpile options that are significant out of a user provided tsconfig +/// file, that we want to deserialize out of the final config for a transpile. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EmitConfigOptions { + pub check_js: bool, + pub emit_decorator_metadata: bool, + pub imports_not_used_as_values: String, + pub inline_source_map: bool, + pub jsx: String, + pub jsx_factory: String, + pub jsx_fragment_factory: String, +} + +/// A structure that represents a set of options that were ignored and the +/// path those options came from. +#[derive(Debug, Clone, PartialEq)] +pub struct IgnoredCompilerOptions { + pub items: Vec<String>, + pub maybe_path: Option<PathBuf>, +} + +impl fmt::Display for IgnoredCompilerOptions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut codes = self.items.clone(); + codes.sort(); + if let Some(path) = &self.maybe_path { + write!(f, "Unsupported compiler options in \"{}\".\n The following options were ignored:\n {}", path.to_string_lossy(), codes.join(", ")) + } else { + write!(f, "Unsupported compiler options provided.\n The following options were ignored:\n {}", codes.join(", ")) + } + } +} + +impl Serialize for IgnoredCompilerOptions { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + Serialize::serialize(&self.items, serializer) + } +} + +/// A static slice of all the compiler options that should be ignored that +/// either have no effect on the compilation or would cause the emit to not work +/// in Deno. +pub const IGNORED_COMPILER_OPTIONS: &[&str] = &[ + "allowSyntheticDefaultImports", + "allowUmdGlobalAccess", + "baseUrl", + "declaration", + "declarationMap", + "downlevelIteration", + "esModuleInterop", + "emitDeclarationOnly", + "importHelpers", + "inlineSourceMap", + "inlineSources", + "module", + "noEmitHelpers", + "noErrorTruncation", + "noLib", + "noResolve", + "outDir", + "paths", + "preserveConstEnums", + "reactNamespace", + "rootDir", + "rootDirs", + "skipLibCheck", + "sourceMap", + "sourceRoot", + "target", + "types", + "useDefineForClassFields", +]; + +pub const IGNORED_RUNTIME_COMPILER_OPTIONS: &[&str] = &[ + "assumeChangesOnlyAffectDirectDependencies", + "build", + "charset", + "composite", + "diagnostics", + "disableSizeLimit", + "emitBOM", + "extendedDiagnostics", + "forceConsistentCasingInFileNames", + "generateCpuProfile", + "help", + "incremental", + "init", + "isolatedModules", + "listEmittedFiles", + "listFiles", + "mapRoot", + "maxNodeModuleJsDepth", + "moduleResolution", + "newLine", + "noEmit", + "noEmitOnError", + "out", + "outDir", + "outFile", + "preserveSymlinks", + "preserveWatchOutput", + "pretty", + "project", + "resolveJsonModule", + "showConfig", + "skipDefaultLibCheck", + "stripInternal", + "traceResolution", + "tsBuildInfoFile", + "typeRoots", + "useDefineForClassFields", + "version", + "watch", +]; + +/// A function that works like JavaScript's `Object.assign()`. +pub fn json_merge(a: &mut Value, b: &Value) { + match (a, b) { + (&mut Value::Object(ref mut a), &Value::Object(ref b)) => { + for (k, v) in b { + json_merge(a.entry(k.clone()).or_insert(Value::Null), v); + } + } + (a, b) => { + *a = b.clone(); + } + } +} + +fn parse_compiler_options( + compiler_options: &HashMap<String, Value>, + maybe_path: Option<PathBuf>, + is_runtime: bool, +) -> Result<(Value, Option<IgnoredCompilerOptions>), AnyError> { + let mut filtered: HashMap<String, Value> = HashMap::new(); + let mut items: Vec<String> = Vec::new(); + + for (key, value) in compiler_options.iter() { + let key = key.as_str(); + if (!is_runtime && IGNORED_COMPILER_OPTIONS.contains(&key)) + || IGNORED_RUNTIME_COMPILER_OPTIONS.contains(&key) + { + items.push(key.to_string()); + } else { + filtered.insert(key.to_string(), value.to_owned()); + } + } + let value = serde_json::to_value(filtered)?; + let maybe_ignored_options = if !items.is_empty() { + Some(IgnoredCompilerOptions { items, maybe_path }) + } else { + None + }; + + Ok((value, maybe_ignored_options)) +} + +/// A structure for managing the configuration of TypeScript +#[derive(Debug, Clone)] +pub struct TsConfig(pub Value); + +impl TsConfig { + /// Create a new `TsConfig` with the base being the `value` supplied. + pub fn new(value: Value) -> Self { + TsConfig(value) + } + + pub fn as_bytes(&self) -> Vec<u8> { + let map = self.0.as_object().unwrap(); + let ordered: BTreeMap<_, _> = map.iter().collect(); + let value = json!(ordered); + value.to_string().as_bytes().to_owned() + } + + /// Return the value of the `checkJs` compiler option, defaulting to `false` + /// if not present. + pub fn get_check_js(&self) -> bool { + if let Some(check_js) = self.0.get("checkJs") { + check_js.as_bool().unwrap_or(false) + } else { + false + } + } + + pub fn get_declaration(&self) -> bool { + if let Some(declaration) = self.0.get("declaration") { + declaration.as_bool().unwrap_or(false) + } else { + false + } + } + + /// Merge a serde_json value into the configuration. + pub fn merge(&mut self, value: &Value) { + json_merge(&mut self.0, value); + } + + /// Take an optional user provided config file + /// which was passed in via the `--config` flag and merge `compilerOptions` with + /// the configuration. Returning the result which optionally contains any + /// compiler options that were ignored. + pub fn merge_tsconfig_from_config_file( + &mut self, + maybe_config_file: Option<&ConfigFile>, + ) -> Result<Option<IgnoredCompilerOptions>, AnyError> { + if let Some(config_file) = maybe_config_file { + let (value, maybe_ignored_options) = config_file.as_compiler_options()?; + self.merge(&value); + Ok(maybe_ignored_options) + } else { + Ok(None) + } + } + + /// Take a map of compiler options, filtering out any that are ignored, then + /// merge it with the current configuration, returning any options that might + /// have been ignored. + pub fn merge_user_config( + &mut self, + user_options: &HashMap<String, Value>, + ) -> Result<Option<IgnoredCompilerOptions>, AnyError> { + let (value, maybe_ignored_options) = + parse_compiler_options(user_options, None, true)?; + self.merge(&value); + Ok(maybe_ignored_options) + } +} + +impl Serialize for TsConfig { + /// Serializes inner hash map which is ordered by the key + fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> + where + S: Serializer, + { + Serialize::serialize(&self.0, serializer) + } +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfigFileJson { + pub compiler_options: Option<Value>, +} + +#[derive(Clone, Debug)] +pub struct ConfigFile { + pub path: PathBuf, + pub json: ConfigFileJson, +} + +impl ConfigFile { + pub fn read(path: &str) -> Result<Self, AnyError> { + let cwd = std::env::current_dir()?; + let config_file = cwd.join(path); + let config_path = canonicalize_path(&config_file).map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!( + "Could not find the config file: {}", + config_file.to_string_lossy() + ), + ) + })?; + let config_text = std::fs::read_to_string(config_path.clone())?; + Self::new(&config_text, &config_path) + } + + pub fn new(text: &str, path: &Path) -> Result<Self, AnyError> { + let jsonc = jsonc_parser::parse_to_serde_value(text)?.unwrap(); + let json: ConfigFileJson = serde_json::from_value(jsonc)?; + + Ok(Self { + path: path.to_owned(), + json, + }) + } + + /// Parse `compilerOptions` and return a serde `Value`. + /// The result also contains any options that were ignored. + pub fn as_compiler_options( + &self, + ) -> Result<(Value, Option<IgnoredCompilerOptions>), AnyError> { + if let Some(compiler_options) = self.json.compiler_options.clone() { + let options: HashMap<String, Value> = + serde_json::from_value(compiler_options) + .context("compilerOptions should be an object")?; + parse_compiler_options(&options, Some(self.path.to_owned()), false) + } else { + Ok((json!({}), None)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use deno_core::serde_json::json; + + #[test] + fn read_config_file() { + let config_file = ConfigFile::read("tests/module_graph/tsconfig.json") + .expect("Failed to load config file"); + assert!(config_file.json.compiler_options.is_some()); + } + + #[test] + fn test_json_merge() { + let mut value_a = json!({ + "a": true, + "b": "c" + }); + let value_b = json!({ + "b": "d", + "e": false, + }); + json_merge(&mut value_a, &value_b); + assert_eq!( + value_a, + json!({ + "a": true, + "b": "d", + "e": false, + }) + ); + } + + #[test] + fn test_parse_config() { + let config_text = r#"{ + "compilerOptions": { + "build": true, + // comments are allowed + "strict": true + } + }"#; + let config_path = PathBuf::from("/deno/tsconfig.json"); + let config_file = ConfigFile::new(config_text, &config_path).unwrap(); + let (options_value, ignored) = + config_file.as_compiler_options().expect("error parsing"); + assert!(options_value.is_object()); + let options = options_value.as_object().unwrap(); + assert!(options.contains_key("strict")); + assert_eq!(options.len(), 1); + assert_eq!( + ignored, + Some(IgnoredCompilerOptions { + items: vec!["build".to_string()], + maybe_path: Some(config_path), + }), + ); + } + + #[test] + fn test_tsconfig_merge_user_options() { + let mut tsconfig = TsConfig::new(json!({ + "target": "esnext", + "module": "esnext", + })); + let user_options = serde_json::from_value(json!({ + "target": "es6", + "build": true, + "strict": false, + })) + .expect("could not convert to hashmap"); + let maybe_ignored_options = tsconfig + .merge_user_config(&user_options) + .expect("could not merge options"); + assert_eq!( + tsconfig.0, + json!({ + "module": "esnext", + "target": "es6", + "strict": false, + }) + ); + assert_eq!( + maybe_ignored_options, + Some(IgnoredCompilerOptions { + items: vec!["build".to_string()], + maybe_path: None + }) + ); + } + + #[test] + fn test_tsconfig_as_bytes() { + let mut tsconfig1 = TsConfig::new(json!({ + "strict": true, + "target": "esnext", + })); + tsconfig1.merge(&json!({ + "target": "es5", + "module": "amd", + })); + let mut tsconfig2 = TsConfig::new(json!({ + "target": "esnext", + "strict": true, + })); + tsconfig2.merge(&json!({ + "module": "amd", + "target": "es5", + })); + assert_eq!(tsconfig1.as_bytes(), tsconfig2.as_bytes()); + } +} |