diff --git a/Cargo.lock b/Cargo.lock index f0784bc71f..fe1743063b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -353,6 +353,7 @@ dependencies = [ "http", "idna", "indexmap", + "jsonc-parser", "lazy_static", "libc", "log 0.4.11", @@ -1024,6 +1025,12 @@ dependencies = [ "swc_common", ] +[[package]] +name = "jsonc-parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce9b3e88481b91c43f37e742879a70dd5855e59f736bf3cac9b1383d68c1186" + [[package]] name = "kernel32-sys" version = "0.2.2" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 7d81f12f6a..e0f75da15b 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -45,6 +45,7 @@ futures = "0.3.5" http = "0.2.1" idna = "0.2.0" indexmap = "1.5.1" +jsonc-parser = "0.14.0" lazy_static = "1.4.0" libc = "0.2.74" log = "0.4.11" diff --git a/cli/main.rs b/cli/main.rs index b194657133..d6b74d8a2a 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -63,6 +63,7 @@ mod test_runner; mod text_encoding; mod tokio_util; mod tsc; +mod tsc_config; mod upgrade; pub mod version; mod web_worker; diff --git a/cli/tsc.rs b/cli/tsc.rs index d509d99cea..cc902d196d 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -1,4 +1,5 @@ // Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + use crate::colors; use crate::diagnostics::Diagnostic; use crate::diagnostics::DiagnosticItem; @@ -20,6 +21,7 @@ use crate::state::State; use crate::swc_util::AstParser; use crate::swc_util::Location; use crate::swc_util::SwcDiagnosticBuffer; +use crate::tsc_config; use crate::version; use crate::worker::Worker; use core::task::Context; @@ -43,6 +45,7 @@ use std::fs; use std::io; use std::ops::Deref; use std::ops::DerefMut; +use std::path::Path; use std::path::PathBuf; use std::pin::Pin; use std::rc::Rc; @@ -182,9 +185,6 @@ impl Future for CompilerWorker { } lazy_static! { - // TODO(bartlomieju): use JSONC parser from dprint instead of Regex - static ref CHECK_JS_RE: Regex = - Regex::new(r#""checkJs"\s*?:\s*?true"#).unwrap(); static ref DENO_TYPES_RE: Regex = Regex::new(r"^\s*@deno-types\s?=\s?(\S+)\s*(.*)\s*$").unwrap(); // These regexes were adapted from TypeScript @@ -199,6 +199,19 @@ lazy_static! { Regex::new(r#"(\slib\s*=\s*)('|")(.+?)('|")"#).unwrap(); } +fn warn_ignored_options( + maybe_ignored_options: Option, + config_path: &Path, +) { + if let Some(ignored_options) = maybe_ignored_options { + eprintln!( + "Unsupported compiler options in \"{}\"\n The following options were ignored:\n {}", + config_path.to_string_lossy(), + ignored_options + ); + } +} + /// Create a new worker with snapshot of TS compiler and setup compiler's /// runtime. fn create_compiler_worker( @@ -241,70 +254,65 @@ pub enum TargetLib { #[derive(Clone)] pub struct CompilerConfig { pub path: Option, - pub content: Option>, - pub hash: Vec, + pub options: Value, + pub maybe_ignored_options: Option, + pub hash: String, pub compile_js: bool, } impl CompilerConfig { /// Take the passed flag and resolve the file name relative to the cwd. - pub fn load(config_path: Option) -> Result { - let config_file = match &config_path { - Some(config_file_name) => { - debug!("Compiler config file: {}", config_file_name); - let cwd = std::env::current_dir().unwrap(); - Some(cwd.join(config_file_name)) - } - _ => None, - }; + pub fn load(maybe_config_path: Option) -> Result { + if maybe_config_path.is_none() { + return Ok(Self { + path: Some(PathBuf::new()), + options: json!({}), + maybe_ignored_options: None, + hash: "".to_string(), + compile_js: false, + }); + } + + let raw_config_path = maybe_config_path.unwrap(); + debug!("Compiler config file: {}", raw_config_path); + let cwd = std::env::current_dir().unwrap(); + let config_file = cwd.join(raw_config_path); // Convert the PathBuf to a canonicalized string. This is needed by the // compiler to properly deal with the configuration. - let config_path = match &config_file { - Some(config_file) => Some(config_file.canonicalize().map_err(|_| { - io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "Could not find the config file: {}", - config_file.to_string_lossy() - ), - ) - })), - _ => None, - }; + let config_path = config_file.canonicalize().map_err(|_| { + io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "Could not find the config file: {}", + config_file.to_string_lossy() + ), + ) + })?; // Load the contents of the configuration file - let config = match &config_file { - Some(config_file) => { - debug!("Attempt to load config: {}", config_file.to_str().unwrap()); - let config = fs::read(&config_file)?; - Some(config) - } - _ => None, - }; + debug!("Attempt to load config: {}", config_path.to_str().unwrap()); + let config_bytes = fs::read(&config_file)?; + let config_hash = crate::checksum::gen(&[&config_bytes]); + let config_str = String::from_utf8(config_bytes)?; - let config_hash = match &config { - Some(bytes) => bytes.clone(), - _ => b"".to_vec(), + let (options, maybe_ignored_options) = if config_str.is_empty() { + (json!({}), None) + } else { + tsc_config::parse_config(&config_str)? }; // If `checkJs` is set to true in `compilerOptions` then we're gonna be compiling // JavaScript files as well - let compile_js = if let Some(config_content) = config.clone() { - let config_str = std::str::from_utf8(&config_content)?; - CHECK_JS_RE.is_match(config_str) - } else { - false - }; + let compile_js = options["checkJs"].as_bool().unwrap_or(false); - let ts_config = Self { - path: config_path.unwrap_or_else(|| Ok(PathBuf::new())).ok(), - content: config, + Ok(Self { + path: Some(config_path), + options, + maybe_ignored_options, hash: config_hash, compile_js, - }; - - Ok(ts_config) + }) } } @@ -471,7 +479,7 @@ impl TsCompiler { let version_hash_to_validate = source_code_version_hash( &source_file.source_code.as_bytes(), version::DENO, - &self.config.hash, + &self.config.hash.as_bytes(), ); if metadata.version_hash == version_hash_to_validate { @@ -577,40 +585,58 @@ impl TsCompiler { let unstable = self.flags.unstable; let performance = matches!(self.flags.log_level, Some(Level::Debug)); let compiler_config = self.config.clone(); - let cwd = std::env::current_dir().unwrap(); - - let j = match (compiler_config.path, compiler_config.content) { - (Some(config_path), Some(config_data)) => json!({ - "type": msg::CompilerRequestType::Compile, - "allowJs": allow_js, - "target": target, - "rootNames": root_names, - "unstable": unstable, - "performance": performance, - "configPath": config_path, - "config": str::from_utf8(&config_data).unwrap(), - "cwd": cwd, - "sourceFileMap": module_graph_json, - "buildInfo": if self.use_disk_cache { build_info } else { None }, - }), - _ => json!({ - "type": msg::CompilerRequestType::Compile, - "allowJs": allow_js, - "target": target, - "rootNames": root_names, - "unstable": unstable, - "performance": performance, - "cwd": cwd, - "sourceFileMap": module_graph_json, - "buildInfo": if self.use_disk_cache { build_info } else { None }, - }), - }; - - let req_msg = j.to_string(); // TODO(bartlomieju): lift this call up - TSC shouldn't print anything info!("{} {}", colors::green("Check"), module_url.to_string()); + let mut lib = if target == "main" { + vec!["deno.window"] + } else { + vec!["deno.worker"] + }; + + if unstable { + lib.push("deno.unstable"); + } + + let mut compiler_options = json!({ + "allowJs": allow_js, + "allowNonTsExtensions": true, + "checkJs": false, + "esModuleInterop": true, + "incremental": true, + "inlineSourceMap": true, + "jsx": "react", + "lib": lib, + "module": "esnext", + "outDir": "deno://", + "resolveJsonModule": true, + "sourceMap": false, + "strict": true, + "removeComments": true, + "target": "esnext", + "tsBuildInfoFile": "cache:///tsbuildinfo.json", + }); + + tsc_config::json_merge(&mut compiler_options, &compiler_config.options); + + warn_ignored_options( + compiler_config.maybe_ignored_options, + compiler_config.path.as_ref().unwrap(), + ); + + let j = json!({ + "type": msg::CompilerRequestType::Compile, + "target": target, + "rootNames": root_names, + "performance": performance, + "compilerOptions": compiler_options, + "sourceFileMap": module_graph_json, + "buildInfo": if self.use_disk_cache { build_info } else { None }, + }); + + let req_msg = j.to_string(); + let json_str = execute_in_same_thread(global_state, permissions, req_msg).await?; @@ -680,36 +706,54 @@ impl TsCompiler { let root_names = vec![module_specifier.to_string()]; let target = "main"; - let cwd = std::env::current_dir().unwrap(); let performance = matches!(global_state.flags.log_level, Some(Level::Debug)); + let unstable = self.flags.unstable; + + let mut lib = if target == "main" { + vec!["deno.window"] + } else { + vec!["deno.worker"] + }; + + if unstable { + lib.push("deno.unstable"); + } + + let mut compiler_options = json!({ + "allowJs": true, + "allowNonTsExtensions": true, + "checkJs": false, + "esModuleInterop": true, + "inlineSourceMap": false, + "jsx": "react", + "lib": lib, + "module": "system", + "outFile": "deno:///bundle.js", + // disabled until we have effective way to modify source maps + "sourceMap": false, + "strict": true, + "removeComments": true, + "target": "esnext", + }); let compiler_config = self.config.clone(); - // TODO(bartlomieju): this is non-sense; CompilerConfig's `path` and `content` should - // be optional - let j = match (compiler_config.path, compiler_config.content) { - (Some(config_path), Some(config_data)) => json!({ - "type": msg::CompilerRequestType::Bundle, - "target": target, - "rootNames": root_names, - "unstable": self.flags.unstable, - "performance": performance, - "configPath": config_path, - "config": str::from_utf8(&config_data).unwrap(), - "cwd": cwd, - "sourceFileMap": module_graph_json, - }), - _ => json!({ - "type": msg::CompilerRequestType::Bundle, - "target": target, - "rootNames": root_names, - "unstable": self.flags.unstable, - "performance": performance, - "cwd": cwd, - "sourceFileMap": module_graph_json, - }), - }; + tsc_config::json_merge(&mut compiler_options, &compiler_config.options); + + warn_ignored_options( + compiler_config.maybe_ignored_options, + compiler_config.path.as_ref().unwrap(), + ); + + let j = json!({ + "type": msg::CompilerRequestType::Bundle, + "target": target, + "rootNames": root_names, + "performance": performance, + "compilerOptions": compiler_options, + "sourceFileMap": module_graph_json, + }); let req_msg = j.to_string(); @@ -900,7 +944,7 @@ impl TsCompiler { let version_hash = source_code_version_hash( &source_file.source_code.as_bytes(), version::DENO, - &self.config.hash, + &self.config.hash.as_bytes(), ); let compiled_file_metadata = CompiledFileMetadata { version_hash }; @@ -1069,7 +1113,7 @@ async fn create_runtime_module_graph( permissions: Permissions, root_name: &str, sources: &Option>, - maybe_options: &Option, + type_files: Vec, ) -> Result<(Vec, ModuleGraph), ErrBox> { let mut root_names = vec![]; let mut module_graph_loader = ModuleGraphLoader::new( @@ -1093,23 +1137,12 @@ async fn create_runtime_module_graph( } // download all additional files from TSconfig and add them to root_names - if let Some(options) = maybe_options { - let options_json: serde_json::Value = serde_json::from_str(options)?; - if let Some(types_option) = options_json.get("types") { - let types_arr = types_option.as_array().expect("types is not an array"); - - for type_value in types_arr { - let type_str = type_value - .as_str() - .expect("type is not a string") - .to_string(); - let type_specifier = ModuleSpecifier::resolve_url_or_path(&type_str)?; - module_graph_loader - .add_to_graph(&type_specifier, None) - .await?; - root_names.push(type_specifier.to_string()) - } - } + for type_file in type_files { + let type_specifier = ModuleSpecifier::resolve_url_or_path(&type_file)?; + module_graph_loader + .add_to_graph(&type_specifier, None) + .await?; + root_names.push(type_specifier.to_string()) } Ok((root_names, module_graph_loader.get_graph())) @@ -1135,12 +1168,65 @@ pub async fn runtime_compile( sources: &Option>, maybe_options: &Option, ) -> Result { + let mut user_options = if let Some(options) = maybe_options { + tsc_config::parse_raw_config(options)? + } else { + json!({}) + }; + + // Intentionally calling "take()" to replace value with `null` - otherwise TSC will try to load that file + // using `fileExists` API + let type_files = if let Some(types) = user_options["types"].take().as_array() + { + types + .iter() + .map(|type_value| type_value.as_str().unwrap_or("").to_string()) + .filter(|type_str| !type_str.is_empty()) + .collect() + } else { + vec![] + }; + + let unstable = global_state.flags.unstable; + + let mut lib = vec![]; + if let Some(user_libs) = user_options["lib"].take().as_array() { + let libs = user_libs + .iter() + .map(|type_value| type_value.as_str().unwrap_or("").to_string()) + .filter(|type_str| !type_str.is_empty()) + .collect::>(); + lib.extend(libs); + } else { + lib.push("deno.window".to_string()); + } + + if unstable { + lib.push("deno.unstable".to_string()); + } + + let mut compiler_options = json!({ + "allowJs": false, + "allowNonTsExtensions": true, + "checkJs": false, + "esModuleInterop": true, + "jsx": "react", + "module": "esnext", + "sourceMap": true, + "strict": true, + "removeComments": true, + "target": "esnext", + }); + + tsc_config::json_merge(&mut compiler_options, &user_options); + tsc_config::json_merge(&mut compiler_options, &json!({ "lib": lib })); + let (root_names, module_graph) = create_runtime_module_graph( &global_state, permissions.clone(), root_name, sources, - maybe_options, + type_files, ) .await?; let module_graph_json = @@ -1151,8 +1237,7 @@ pub async fn runtime_compile( "target": "runtime", "rootNames": root_names, "sourceFileMap": module_graph_json, - "options": maybe_options, - "unstable": global_state.flags.unstable, + "compilerOptions": compiler_options, }) .to_string(); @@ -1182,24 +1267,88 @@ pub async fn runtime_bundle( sources: &Option>, maybe_options: &Option, ) -> Result { + let mut user_options = if let Some(options) = maybe_options { + tsc_config::parse_raw_config(options)? + } else { + json!({}) + }; + + // Intentionally calling "take()" to replace value with `null` - otherwise TSC will try to load that file + // using `fileExists` API + let type_files = if let Some(types) = user_options["types"].take().as_array() + { + types + .iter() + .map(|type_value| type_value.as_str().unwrap_or("").to_string()) + .filter(|type_str| !type_str.is_empty()) + .collect() + } else { + vec![] + }; + let (root_names, module_graph) = create_runtime_module_graph( &global_state, permissions.clone(), root_name, sources, - maybe_options, + type_files, ) .await?; let module_graph_json = serde_json::to_value(module_graph).expect("Failed to serialize data"); + let unstable = global_state.flags.unstable; + + let mut lib = vec![]; + if let Some(user_libs) = user_options["lib"].take().as_array() { + let libs = user_libs + .iter() + .map(|type_value| type_value.as_str().unwrap_or("").to_string()) + .filter(|type_str| !type_str.is_empty()) + .collect::>(); + lib.extend(libs); + } else { + lib.push("deno.window".to_string()); + } + + if unstable { + lib.push("deno.unstable".to_string()); + } + + let mut compiler_options = json!({ + "allowJs": false, + "allowNonTsExtensions": true, + "checkJs": false, + "esModuleInterop": true, + "jsx": "react", + "module": "esnext", + "outDir": null, + "sourceMap": true, + "strict": true, + "removeComments": true, + "target": "esnext", + }); + + let bundler_options = json!({ + "allowJs": true, + "inlineSourceMap": false, + "module": "system", + "outDir": null, + "outFile": "deno:///bundle.js", + // disabled until we have effective way to modify source maps + "sourceMap": false, + }); + + tsc_config::json_merge(&mut compiler_options, &user_options); + tsc_config::json_merge(&mut compiler_options, &json!({ "lib": lib })); + tsc_config::json_merge(&mut compiler_options, &bundler_options); + let req_msg = json!({ "type": msg::CompilerRequestType::RuntimeBundle, "target": "runtime", "rootNames": root_names, "sourceFileMap": module_graph_json, - "options": maybe_options, - "unstable": global_state.flags.unstable, + "compilerOptions": compiler_options, }) .to_string(); @@ -1218,12 +1367,27 @@ pub async fn runtime_transpile( global_state: &Arc, permissions: Permissions, sources: &HashMap, - options: &Option, + maybe_options: &Option, ) -> Result { + let user_options = if let Some(options) = maybe_options { + tsc_config::parse_raw_config(options)? + } else { + json!({}) + }; + + let mut compiler_options = json!({ + "esModuleInterop": true, + "module": "esnext", + "sourceMap": true, + "scriptComments": true, + "target": "esnext", + }); + tsc_config::json_merge(&mut compiler_options, &user_options); + let req_msg = json!({ "type": msg::CompilerRequestType::RuntimeTranspile, "sources": sources, - "options": options, + "compilerOptions": compiler_options, }) .to_string(); @@ -1754,11 +1918,14 @@ mod tests { (r#"{ "compilerOptions": { "checkJs": true } } "#, true), // JSON with comment ( - r#"{ "compilerOptions": { // force .js file compilation by Deno "checkJs": true } } "#, + r#"{ + "compilerOptions": { + // force .js file compilation by Deno + "checkJs": true + } + }"#, true, ), - // invalid JSON - (r#"{ "compilerOptions": { "checkJs": true },{ } "#, true), // without content ("", false), ]; diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index 1bef2cf650..79af46e31f 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -303,95 +303,6 @@ delete Object.prototype.__proto__; // file are passed back to Rust and saved to $DENO_DIR. const TS_BUILD_INFO = "cache:///tsbuildinfo.json"; - // TODO(Bartlomieju): this check should be done in Rust - const IGNORED_COMPILER_OPTIONS = [ - "allowSyntheticDefaultImports", - "allowUmdGlobalAccess", - "assumeChangesOnlyAffectDirectDependencies", - "baseUrl", - "build", - "composite", - "declaration", - "declarationDir", - "declarationMap", - "diagnostics", - "downlevelIteration", - "emitBOM", - "emitDeclarationOnly", - "esModuleInterop", - "extendedDiagnostics", - "forceConsistentCasingInFileNames", - "generateCpuProfile", - "help", - "importHelpers", - "incremental", - "inlineSourceMap", - "inlineSources", - "init", - "listEmittedFiles", - "listFiles", - "mapRoot", - "maxNodeModuleJsDepth", - "module", - "moduleResolution", - "newLine", - "noEmit", - "noEmitHelpers", - "noEmitOnError", - "noLib", - "noResolve", - "out", - "outDir", - "outFile", - "paths", - "preserveSymlinks", - "preserveWatchOutput", - "pretty", - "rootDir", - "rootDirs", - "showConfig", - "skipDefaultLibCheck", - "skipLibCheck", - "sourceMap", - "sourceRoot", - "stripInternal", - "target", - "traceResolution", - "tsBuildInfoFile", - "types", - "typeRoots", - "version", - "watch", - ]; - - const DEFAULT_BUNDLER_OPTIONS = { - allowJs: true, - inlineSourceMap: false, - module: ts.ModuleKind.System, - outDir: undefined, - outFile: `${OUT_DIR}/bundle.js`, - // disabled until we have effective way to modify source maps - sourceMap: false, - }; - - const DEFAULT_INCREMENTAL_COMPILE_OPTIONS = { - allowJs: false, - allowNonTsExtensions: true, - checkJs: false, - esModuleInterop: true, - incremental: true, - inlineSourceMap: true, - jsx: ts.JsxEmit.React, - module: ts.ModuleKind.ESNext, - outDir: OUT_DIR, - resolveJsonModule: true, - sourceMap: false, - strict: true, - stripComments: true, - target: ts.ScriptTarget.ESNext, - tsBuildInfoFile: TS_BUILD_INFO, - }; - const DEFAULT_COMPILE_OPTIONS = { allowJs: false, allowNonTsExtensions: true, @@ -406,18 +317,6 @@ delete Object.prototype.__proto__; target: ts.ScriptTarget.ESNext, }; - const DEFAULT_RUNTIME_COMPILE_OPTIONS = { - outDir: undefined, - }; - - const DEFAULT_RUNTIME_TRANSPILE_OPTIONS = { - esModuleInterop: true, - module: ts.ModuleKind.ESNext, - sourceMap: true, - scriptComments: true, - target: ts.ScriptTarget.ESNext, - }; - const CompilerHostTarget = { Main: "main", Runtime: "runtime", @@ -481,28 +380,17 @@ delete Object.prototype.__proto__; */ const RESOLVED_SPECIFIER_CACHE = new Map(); - function configure(defaultOptions, source, path, cwd) { - const { config, error } = ts.parseConfigFileTextToJson(path, source); - if (error) { - return { diagnostics: [error], options: defaultOptions }; - } + function parseCompilerOptions(compilerOptions) { + // TODO(bartlomieju): using `/` and `/tsconfig.json` because + // otherwise TSC complains that some paths are relative + // and some are absolute const { options, errors } = ts.convertCompilerOptionsFromJson( - config.compilerOptions, - cwd, + compilerOptions, + "/", + "/tsconfig.json", ); - const ignoredOptions = []; - for (const key of Object.keys(options)) { - if ( - IGNORED_COMPILER_OPTIONS.includes(key) && - (!(key in defaultOptions) || options[key] !== defaultOptions[key]) - ) { - ignoredOptions.push(key); - delete options[key]; - } - } return { - options: Object.assign({}, defaultOptions, options), - ignoredOptions: ignoredOptions.length ? ignoredOptions : undefined, + options, diagnostics: errors.length ? errors : undefined, }; } @@ -567,57 +455,25 @@ delete Object.prototype.__proto__; } class Host { - #options = DEFAULT_COMPILE_OPTIONS; - #target = ""; - #writeFile = null; + #options; + #target; + #writeFile; /* Deno specific APIs */ - constructor({ - bundle = false, - incremental = false, + constructor( + options, target, - unstable, writeFile, - }) { + ) { this.#target = target; this.#writeFile = writeFile; - if (bundle) { - // options we need to change when we are generating a bundle - Object.assign(this.#options, DEFAULT_BUNDLER_OPTIONS); - } else if (incremental) { - Object.assign(this.#options, DEFAULT_INCREMENTAL_COMPILE_OPTIONS); - } - if (unstable) { - this.#options.lib = [ - target === CompilerHostTarget.Worker - ? "lib.deno.worker.d.ts" - : "lib.deno.window.d.ts", - "lib.deno.unstable.d.ts", - ]; - } + this.#options = options; } get options() { return this.#options; } - configure(cwd, path, configurationText) { - log("compiler::host.configure", path); - const { options, ...result } = configure( - this.#options, - configurationText, - path, - cwd, - ); - this.#options = options; - return result; - } - - mergeOptions(...options) { - Object.assign(this.#options, ...options); - return Object.assign({}, this.#options); - } - /* TypeScript CompilerHost APIs */ fileExists(_fileName) { @@ -742,9 +598,13 @@ delete Object.prototype.__proto__; class IncrementalCompileHost extends Host { #buildInfo = ""; - constructor(options) { - super({ ...options, incremental: true }); - const { buildInfo } = options; + constructor( + options, + target, + writeFile, + buildInfo, + ) { + super(options, target, writeFile); if (buildInfo) { this.#buildInfo = buildInfo; } @@ -761,10 +621,11 @@ delete Object.prototype.__proto__; // NOTE: target doesn't really matter here, // this is in fact a mock host created just to // load all type definitions and snapshot them. - let SNAPSHOT_HOST = new Host({ - target: CompilerHostTarget.Main, - writeFile() {}, - }); + let SNAPSHOT_HOST = new Host( + DEFAULT_COMPILE_OPTIONS, + CompilerHostTarget.Main, + () => {}, + ); const SNAPSHOT_COMPILER_OPTIONS = SNAPSHOT_HOST.getCompilationSettings(); // This is a hacky way of adding our libs to the libs available in TypeScript() @@ -985,101 +846,7 @@ delete Object.prototype.__proto__; }; } - function convertCompilerOptions(str) { - const options = JSON.parse(str); - const out = {}; - const keys = Object.keys(options); - const files = []; - for (const key of keys) { - switch (key) { - case "jsx": - const value = options[key]; - if (value === "preserve") { - out[key] = ts.JsxEmit.Preserve; - } else if (value === "react") { - out[key] = ts.JsxEmit.React; - } else { - out[key] = ts.JsxEmit.ReactNative; - } - break; - case "module": - switch (options[key]) { - case "amd": - out[key] = ts.ModuleKind.AMD; - break; - case "commonjs": - out[key] = ts.ModuleKind.CommonJS; - break; - case "es2015": - case "es6": - out[key] = ts.ModuleKind.ES2015; - break; - case "esnext": - out[key] = ts.ModuleKind.ESNext; - break; - case "none": - out[key] = ts.ModuleKind.None; - break; - case "system": - out[key] = ts.ModuleKind.System; - break; - case "umd": - out[key] = ts.ModuleKind.UMD; - break; - default: - throw new TypeError("Unexpected module type"); - } - break; - case "target": - switch (options[key]) { - case "es3": - out[key] = ts.ScriptTarget.ES3; - break; - case "es5": - out[key] = ts.ScriptTarget.ES5; - break; - case "es6": - case "es2015": - out[key] = ts.ScriptTarget.ES2015; - break; - case "es2016": - out[key] = ts.ScriptTarget.ES2016; - break; - case "es2017": - out[key] = ts.ScriptTarget.ES2017; - break; - case "es2018": - out[key] = ts.ScriptTarget.ES2018; - break; - case "es2019": - out[key] = ts.ScriptTarget.ES2019; - break; - case "es2020": - out[key] = ts.ScriptTarget.ES2020; - break; - case "esnext": - out[key] = ts.ScriptTarget.ESNext; - break; - default: - throw new TypeError("Unexpected emit target."); - } - break; - case "types": - const types = options[key]; - assert(types); - files.push(...types); - break; - default: - out[key] = options[key]; - } - } - return { - options: out, - files: files.length ? files : undefined, - }; - } - - const ignoredDiagnostics = [ + const IGNORED_DIAGNOSTICS = [ // TS2306: File 'file:///Users/rld/src/deno/cli/tests/subdir/amd_like.js' is // not a module. 2306, @@ -1158,21 +925,6 @@ delete Object.prototype.__proto__; return stats; } - // TODO(Bartlomieju): this check should be done in Rust; there should be no - function processConfigureResponse(configResult, configPath) { - const { ignoredOptions, diagnostics } = configResult; - if (ignoredOptions) { - const msg = - `Unsupported compiler options in "${configPath}"\n The following options were ignored:\n ${ - ignoredOptions - .map((value) => value) - .join(", ") - }\n`; - core.print(msg, true); - } - return diagnostics; - } - function normalizeString(path) { let res = ""; let lastSegmentLength = 0; @@ -1346,14 +1098,10 @@ delete Object.prototype.__proto__; } function compile({ - allowJs, buildInfo, - config, - configPath, + compilerOptions, rootNames, target, - unstable, - cwd, sourceFileMap, type, performance, @@ -1371,23 +1119,27 @@ delete Object.prototype.__proto__; rootNames, emitMap: {}, }; - const host = new IncrementalCompileHost({ - bundle: false, - target, - unstable, - writeFile: createCompileWriteFile(state), - rootNames, - buildInfo, - }); + let diagnostics = []; - host.mergeOptions({ allowJs }); + const { options, diagnostics: diags } = parseCompilerOptions( + compilerOptions, + ); - // if there is a configuration supplied, we need to parse that - if (config && config.length && configPath) { - const configResult = host.configure(cwd, configPath, config); - diagnostics = processConfigureResponse(configResult, configPath) || []; - } + diagnostics = diags.filter( + ({ code }) => code != 5023 && !IGNORED_DIAGNOSTICS.includes(code), + ); + + // TODO(bartlomieju): this options is excluded by `ts.convertCompilerOptionsFromJson` + // however stuff breaks if it's not passed (type_directives_js_main.js, compiler_js_error.ts) + options.allowNonTsExtensions = true; + + const host = new IncrementalCompileHost( + options, + target, + createCompileWriteFile(state), + buildInfo, + ); buildSourceFileCache(sourceFileMap); // if there was a configuration and no diagnostics with it, we will continue @@ -1409,7 +1161,7 @@ delete Object.prototype.__proto__; ...program.getSemanticDiagnostics(), ]; diagnostics = diagnostics.filter( - ({ code }) => !ignoredDiagnostics.includes(code), + ({ code }) => !IGNORED_DIAGNOSTICS.includes(code), ); // We will only proceed with the emit if there are no diagnostics. @@ -1443,12 +1195,9 @@ delete Object.prototype.__proto__; } function bundle({ - config, - configPath, + compilerOptions, rootNames, target, - unstable, - cwd, sourceFileMap, type, performance, @@ -1469,20 +1218,25 @@ delete Object.prototype.__proto__; rootNames, bundleOutput: undefined, }; - const host = new Host({ - bundle: true, - target, - unstable, - writeFile: createBundleWriteFile(state), - }); - state.host = host; - let diagnostics = []; - // if there is a configuration supplied, we need to parse that - if (config && config.length && configPath) { - const configResult = host.configure(cwd, configPath, config); - diagnostics = processConfigureResponse(configResult, configPath) || []; - } + const { options, diagnostics: diags } = parseCompilerOptions( + compilerOptions, + ); + + diagnostics = diags.filter( + ({ code }) => code != 5023 && !IGNORED_DIAGNOSTICS.includes(code), + ); + + // TODO(bartlomieju): this options is excluded by `ts.convertCompilerOptionsFromJson` + // however stuff breaks if it's not passed (type_directives_js_main.js) + options.allowNonTsExtensions = true; + + const host = new Host( + options, + target, + createBundleWriteFile(state), + ); + state.host = host; buildSourceFileCache(sourceFileMap); // if there was a configuration and no diagnostics with it, we will continue @@ -1497,7 +1251,7 @@ delete Object.prototype.__proto__; diagnostics = ts .getPreEmitDiagnostics(program) - .filter(({ code }) => !ignoredDiagnostics.includes(code)); + .filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code)); // We will only proceed with the emit if there are no diagnostics. if (diagnostics.length === 0) { @@ -1542,7 +1296,7 @@ delete Object.prototype.__proto__; } function runtimeCompile(request) { - const { options, rootNames, target, unstable, sourceFileMap } = request; + const { compilerOptions, rootNames, target, sourceFileMap } = request; log(">>> runtime compile start", { rootNames, @@ -1550,11 +1304,13 @@ delete Object.prototype.__proto__; // if there are options, convert them into TypeScript compiler options, // and resolve any external file references - let convertedOptions; - if (options) { - const result = convertCompilerOptions(options); - convertedOptions = result.options; - } + const result = parseCompilerOptions( + compilerOptions, + ); + const options = result.options; + // TODO(bartlomieju): this options is excluded by `ts.convertCompilerOptionsFromJson` + // however stuff breaks if it's not passed (type_directives_js_main.js, compiler_js_error.ts) + options.allowNonTsExtensions = true; buildLocalSourceFileCache(sourceFileMap); @@ -1562,25 +1318,11 @@ delete Object.prototype.__proto__; rootNames, emitMap: {}, }; - const host = new Host({ - bundle: false, + const host = new Host( + options, target, - writeFile: createRuntimeCompileWriteFile(state), - }); - const compilerOptions = [DEFAULT_RUNTIME_COMPILE_OPTIONS]; - if (convertedOptions) { - compilerOptions.push(convertedOptions); - } - if (unstable) { - compilerOptions.push({ - lib: [ - "deno.unstable", - ...((convertedOptions && convertedOptions.lib) || ["deno.window"]), - ], - }); - } - - host.mergeOptions(...compilerOptions); + createRuntimeCompileWriteFile(state), + ); const program = ts.createProgram({ rootNames, @@ -1590,10 +1332,9 @@ delete Object.prototype.__proto__; const diagnostics = ts .getPreEmitDiagnostics(program) - .filter(({ code }) => !ignoredDiagnostics.includes(code)); + .filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code)); const emitResult = program.emit(); - assert(emitResult.emitSkipped === false, "Unexpected skip of the emit."); log("<<< runtime compile finish", { @@ -1612,7 +1353,7 @@ delete Object.prototype.__proto__; } function runtimeBundle(request) { - const { options, rootNames, target, unstable, sourceFileMap } = request; + const { compilerOptions, rootNames, target, sourceFileMap } = request; log(">>> runtime bundle start", { rootNames, @@ -1620,11 +1361,13 @@ delete Object.prototype.__proto__; // if there are options, convert them into TypeScript compiler options, // and resolve any external file references - let convertedOptions; - if (options) { - const result = convertCompilerOptions(options); - convertedOptions = result.options; - } + const result = parseCompilerOptions( + compilerOptions, + ); + const options = result.options; + // TODO(bartlomieju): this options is excluded by `ts.convertCompilerOptionsFromJson` + // however stuff breaks if it's not passed (type_directives_js_main.js, compiler_js_error.ts) + options.allowNonTsExtensions = true; buildLocalSourceFileCache(sourceFileMap); @@ -1632,27 +1375,13 @@ delete Object.prototype.__proto__; rootNames, bundleOutput: undefined, }; - const host = new Host({ - bundle: true, - target, - writeFile: createBundleWriteFile(state), - }); - state.host = host; - const compilerOptions = [DEFAULT_RUNTIME_COMPILE_OPTIONS]; - if (convertedOptions) { - compilerOptions.push(convertedOptions); - } - if (unstable) { - compilerOptions.push({ - lib: [ - "deno.unstable", - ...((convertedOptions && convertedOptions.lib) || ["deno.window"]), - ], - }); - } - compilerOptions.push(DEFAULT_BUNDLER_OPTIONS); - host.mergeOptions(...compilerOptions); + const host = new Host( + options, + target, + createBundleWriteFile(state), + ); + state.host = host; const program = ts.createProgram({ rootNames, @@ -1663,7 +1392,7 @@ delete Object.prototype.__proto__; setRootExports(program, rootNames[0]); const diagnostics = ts .getPreEmitDiagnostics(program) - .filter(({ code }) => !ignoredDiagnostics.includes(code)); + .filter(({ code }) => !IGNORED_DIAGNOSTICS.includes(code)); const emitResult = program.emit(); @@ -1685,21 +1414,22 @@ delete Object.prototype.__proto__; function runtimeTranspile(request) { const result = {}; - const { sources, options } = request; - const compilerOptions = options - ? Object.assign( - {}, - DEFAULT_RUNTIME_TRANSPILE_OPTIONS, - convertCompilerOptions(options).options, - ) - : DEFAULT_RUNTIME_TRANSPILE_OPTIONS; + const { sources, compilerOptions } = request; + + const parseResult = parseCompilerOptions( + compilerOptions, + ); + const options = parseResult.options; + // TODO(bartlomieju): this options is excluded by `ts.convertCompilerOptionsFromJson` + // however stuff breaks if it's not passed (type_directives_js_main.js, compiler_js_error.ts) + options.allowNonTsExtensions = true; for (const [fileName, inputText] of Object.entries(sources)) { const { outputText: source, sourceMapText: map } = ts.transpileModule( inputText, { fileName, - compilerOptions, + compilerOptions: options, }, ); result[fileName] = { source, map }; diff --git a/cli/tsc_config.rs b/cli/tsc_config.rs new file mode 100644 index 0000000000..e5f7bcdc4f --- /dev/null +++ b/cli/tsc_config.rs @@ -0,0 +1,236 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use deno_core::ErrBox; +use jsonc_parser::JsonValue; +use serde::Deserialize; +use serde_json::Value; +use std::collections::HashMap; +use std::fmt; +use std::str::FromStr; + +#[derive(Clone, Debug, PartialEq)] +pub struct IgnoredCompilerOptions(pub Vec); + +impl fmt::Display for IgnoredCompilerOptions { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut codes = self.0.clone(); + codes.sort(); + write!(f, "{}", codes.join(", "))?; + + Ok(()) + } +} + +/// 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. +const IGNORED_COMPILER_OPTIONS: [&str; 61] = [ + "allowSyntheticDefaultImports", + "allowUmdGlobalAccess", + "assumeChangesOnlyAffectDirectDependencies", + "baseUrl", + "build", + "composite", + "declaration", + "declarationDir", + "declarationMap", + "diagnostics", + "downlevelIteration", + "emitBOM", + "emitDeclarationOnly", + "esModuleInterop", + "extendedDiagnostics", + "forceConsistentCasingInFileNames", + "generateCpuProfile", + "help", + "importHelpers", + "incremental", + "inlineSourceMap", + "inlineSources", + "init", + "listEmittedFiles", + "listFiles", + "mapRoot", + "maxNodeModuleJsDepth", + "module", + "moduleResolution", + "newLine", + "noEmit", + "noEmitHelpers", + "noEmitOnError", + "noLib", + "noResolve", + "out", + "outDir", + "outFile", + "paths", + "preserveConstEnums", + "preserveSymlinks", + "preserveWatchOutput", + "pretty", + "reactNamespace", + "resolveJsonModule", + "rootDir", + "rootDirs", + "showConfig", + "skipDefaultLibCheck", + "skipLibCheck", + "sourceMap", + "sourceRoot", + "stripInternal", + "target", + "traceResolution", + "tsBuildInfoFile", + "types", + "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(); + } + } +} + +/// 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, +} + +pub fn parse_raw_config(config_text: &str) -> Result { + assert!(!config_text.is_empty()); + let jsonc = jsonc_parser::parse_to_value(config_text)?.unwrap(); + Ok(jsonc_to_serde(jsonc)) +} + +/// 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, +) -> Result<(Value, Option), ErrBox> { + 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))?; + let mut compiler_options: HashMap = HashMap::new(); + let mut items: Vec = Vec::new(); + + if let Some(in_compiler_options) = config.compiler_options { + for (key, value) in in_compiler_options.iter() { + if IGNORED_COMPILER_OPTIONS.contains(&key.as_str()) { + items.push(key.to_owned()); + } else { + compiler_options.insert(key.to_owned(), value.to_owned()); + } + } + } + let options_value = serde_json::to_value(compiler_options)?; + let ignored_options = if !items.is_empty() { + Some(IgnoredCompilerOptions(items)) + } else { + None + }; + + Ok((options_value, ignored_options)) +} + +#[cfg(test)] +mod tests { + use super::*; + use 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 (options_value, ignored) = + parse_config(config_text).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(vec!["build".to_string()])), + ); + } + + #[test] + fn test_parse_raw_config() { + let invalid_config_text = r#"{ + "compilerOptions": { + // comments are allowed + }"#; + let errbox = parse_raw_config(invalid_config_text).unwrap_err(); + assert!(errbox + .to_string() + .starts_with("Unterminated object on line 1")); + } +}