Skip to main content
Module

x/deno/cli/config_file.rs

A modern runtime for JavaScript and TypeScript.
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use crate::fs_util::canonicalize_path;use deno_core::error::anyhow;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 inline_sources: bool, pub source_map: bool, pub jsx: String, pub jsx_factory: String, pub jsx_fragment_factory: String,}
/// There are certain compiler options that can impact what modules are part of/// a module graph, which need to be deserialized into a structure for analysis.#[derive(Debug, Deserialize)]#[serde(rename_all = "camelCase")]pub struct CompilerOptions { pub types: Option<Vec<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", "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.to_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, Default, Deserialize)]#[serde(default, deny_unknown_fields)]pub struct LintRulesConfig { pub tags: Option<Vec<String>>, pub include: Option<Vec<String>>, pub exclude: Option<Vec<String>>,}
#[derive(Clone, Debug, Default, Deserialize)]#[serde(default, deny_unknown_fields)]pub struct FilesConfig { pub include: Vec<String>, pub exclude: Vec<String>,}
#[derive(Clone, Debug, Default, Deserialize)]#[serde(default, deny_unknown_fields)]pub struct LintConfig { pub rules: LintRulesConfig, pub files: FilesConfig,}
#[derive(Clone, Copy, Debug, Deserialize)]#[serde(deny_unknown_fields, rename_all = "camelCase")]pub enum ProseWrap { Always, Never, Preserve,}
#[derive(Clone, Debug, Default, Deserialize)]#[serde(default, deny_unknown_fields, rename_all = "camelCase")]pub struct FmtOptionsConfig { pub use_tabs: Option<bool>, pub line_width: Option<u32>, pub indent_width: Option<u8>, pub single_quote: Option<bool>, pub prose_wrap: Option<ProseWrap>,}
#[derive(Clone, Debug, Default, Deserialize)]#[serde(default, deny_unknown_fields)]pub struct FmtConfig { pub options: FmtOptionsConfig, pub files: FilesConfig,}
#[derive(Clone, Debug, Deserialize)]#[serde(rename_all = "camelCase")]pub struct ConfigFileJson { pub compiler_options: Option<Value>, pub lint: Option<Value>, pub fmt: Option<Value>,}
#[derive(Clone, Debug)]pub struct ConfigFile { pub path: PathBuf, pub json: ConfigFileJson,}
impl ConfigFile { pub fn read(path_ref: impl AsRef<Path>) -> Result<Self, AnyError> { let path = Path::new(path_ref.as_ref()); let config_file = if path.is_absolute() { path.to_path_buf() } else { std::env::current_dir()?.join(path_ref) };
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 = match jsonc_parser::parse_to_serde_value(text) { Ok(None) => json!({}), Ok(Some(value)) if value.is_object() => value, Ok(Some(_)) => { return Err(anyhow!( "config file JSON {:?} should be an object", path.to_str().unwrap() )) } Err(e) => { return Err(anyhow!( "Unable to parse config file JSON {:?} because of {}", path.to_str().unwrap(), e.to_string() )) } }; 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 to_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)) } }
pub fn to_lint_config(&self) -> Result<Option<LintConfig>, AnyError> { if let Some(config) = self.json.lint.clone() { let lint_config: LintConfig = serde_json::from_value(config) .context("Failed to parse \"lint\" configuration")?; Ok(Some(lint_config)) } else { Ok(None) } }
pub fn to_fmt_config(&self) -> Result<Option<FmtConfig>, AnyError> { if let Some(config) = self.json.fmt.clone() { let fmt_config: FmtConfig = serde_json::from_value(config) .context("Failed to parse \"fmt\" configuration")?; Ok(Some(fmt_config)) } else { Ok(None) } }}
#[cfg(test)]mod tests { use super::*; use deno_core::serde_json::json;
#[test] fn read_config_file_relative() { let config_file = ConfigFile::read("tests/testdata/module_graph/tsconfig.json") .expect("Failed to load config file"); assert!(config_file.json.compiler_options.is_some()); }
#[test] fn read_config_file_absolute() { let path = test_util::testdata_path().join("module_graph/tsconfig.json"); let config_file = ConfigFile::read(path.to_str().unwrap()) .expect("Failed to load config file"); assert!(config_file.json.compiler_options.is_some()); }
#[test] fn include_config_path_on_error() { let error = ConfigFile::read("404.json").err().unwrap(); assert!(error.to_string().contains("404.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 }, "lint": { "files": { "include": ["src/"], "exclude": ["src/testdata/"] }, "rules": { "tags": ["recommended"], "include": ["ban-untagged-todo"] } }, "fmt": { "files": { "include": ["src/"], "exclude": ["src/testdata/"] }, "options": { "useTabs": true, "lineWidth": 80, "indentWidth": 4, "singleQuote": true, "proseWrap": "preserve" } } }"#; let config_path = PathBuf::from("/deno/tsconfig.json"); let config_file = ConfigFile::new(config_text, &config_path).unwrap(); let (options_value, ignored) = config_file.to_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), }), );
let lint_config = config_file .to_lint_config() .expect("error parsing lint object") .expect("lint object should be defined"); assert_eq!(lint_config.files.include, vec!["src/"]); assert_eq!(lint_config.files.exclude, vec!["src/testdata/"]); assert_eq!( lint_config.rules.include, Some(vec!["ban-untagged-todo".to_string()]) ); assert_eq!( lint_config.rules.tags, Some(vec!["recommended".to_string()]) ); assert!(lint_config.rules.exclude.is_none());
let fmt_config = config_file .to_fmt_config() .expect("error parsing fmt object") .expect("fmt object should be defined"); assert_eq!(fmt_config.files.include, vec!["src/"]); assert_eq!(fmt_config.files.exclude, vec!["src/testdata/"]); assert_eq!(fmt_config.options.use_tabs, Some(true)); assert_eq!(fmt_config.options.line_width, Some(80)); assert_eq!(fmt_config.options.indent_width, Some(4)); assert_eq!(fmt_config.options.single_quote, Some(true)); }
#[test] fn test_parse_config_with_empty_file() { let config_text = ""; let config_path = PathBuf::from("/deno/tsconfig.json"); let config_file = ConfigFile::new(config_text, &config_path).unwrap(); let (options_value, _) = config_file.to_compiler_options().expect("error parsing"); assert!(options_value.is_object()); }
#[test] fn test_parse_config_with_commented_file() { let config_text = r#"//{"foo":"bar"}"#; let config_path = PathBuf::from("/deno/tsconfig.json"); let config_file = ConfigFile::new(config_text, &config_path).unwrap(); let (options_value, _) = config_file.to_compiler_options().expect("error parsing"); assert!(options_value.is_object()); }
#[test] fn test_parse_config_with_invalid_file() { let config_text = "{foo:bar}"; let config_path = PathBuf::from("/deno/tsconfig.json"); // Emit error: Unable to parse config file JSON "<config_path>" because of Unexpected token on line 1 column 6. assert!(ConfigFile::new(config_text, &config_path).is_err()); }
#[test] fn test_parse_config_with_not_object_file() { let config_text = "[]"; let config_path = PathBuf::from("/deno/tsconfig.json"); // Emit error: config file JSON "<config_path>" should be an object assert!(ConfigFile::new(config_text, &config_path).is_err()); }
#[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()); }}