// 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::serde_json; use deno_core::serde_json::json; use deno_core::serde_json::Value; use jsonc_parser::JsonValue; use serde::Deserialize; use serde::Serialize; use serde::Serializer; use std::collections::BTreeMap; use std::collections::HashMap; use std::fmt; use std::path::Path; use std::path::PathBuf; use std::str::FromStr; /// 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 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, pub maybe_path: Option, } 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(&self, serializer: S) -> Result 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", "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", "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(); } } } /// Convert a jsonc libraries `JsonValue` to a serde `Value`. fn jsonc_to_serde(j: JsonValue) -> Value { match j { JsonValue::Array(arr) => { let vec = arr.into_iter().map(jsonc_to_serde).collect(); Value::Array(vec) } JsonValue::Boolean(bool) => Value::Bool(bool), JsonValue::Null => Value::Null, JsonValue::Number(num) => { let number = serde_json::Number::from_str(&num).expect("could not parse number"); Value::Number(number) } JsonValue::Object(obj) => { let mut map = serde_json::map::Map::new(); for (key, json_value) in obj.into_iter() { map.insert(key, jsonc_to_serde(json_value)); } Value::Object(map) } JsonValue::String(str) => Value::String(str), } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct TSConfigJson { compiler_options: Option>, exclude: Option>, extends: Option, files: Option>, include: Option>, references: Option, type_acquisition: Option, } fn parse_compiler_options( compiler_options: &HashMap, maybe_path: Option, is_runtime: bool, ) -> Result<(Value, Option), AnyError> { let mut filtered: HashMap = HashMap::new(); let mut items: Vec = 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)) } /// Take a string of JSONC, parse it and return a serde `Value` of the text. /// The result also contains any options that were ignored. pub fn parse_config( config_text: &str, path: &Path, ) -> Result<(Value, Option), AnyError> { assert!(!config_text.is_empty()); let jsonc = jsonc_parser::parse_to_value(config_text)?.unwrap(); let config: TSConfigJson = serde_json::from_value(jsonc_to_serde(jsonc))?; if let Some(compiler_options) = config.compiler_options { parse_compiler_options(&compiler_options, Some(path.to_owned()), false) } else { Ok((json!({}), None)) } } /// 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 { 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 string representing a user provided TypeScript config file /// which was passed in via the `--config` compiler option and merge it with /// the configuration. Returning the result which optionally contains any /// compiler options that were ignored. /// /// When there are options ignored out of the file, a warning will be written /// to stderr regarding the options that were ignored. pub fn merge_tsconfig( &mut self, maybe_path: Option, ) -> Result, AnyError> { if let Some(path) = maybe_path { 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())?; let (value, maybe_ignored_options) = parse_config(&config_text, &config_path)?; json_merge(&mut self.0, &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, ) -> Result, AnyError> { let (value, maybe_ignored_options) = parse_compiler_options(user_options, None, true)?; json_merge(&mut self.0, &value); Ok(maybe_ignored_options) } } impl Serialize for TsConfig { /// Serializes inner hash map which is ordered by the key fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { Serialize::serialize(&self.0, serializer) } } #[cfg(test)] mod tests { use super::*; use deno_core::serde_json::json; #[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 (options_value, ignored) = parse_config(config_text, &config_path).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()); } }