From 81d86e32c80992d0b6bf3940ca63619ff532e152 Mon Sep 17 00:00:00 2001 From: Kitson Kelly Date: Fri, 26 Apr 2019 11:59:42 +0100 Subject: [PATCH] Add support for loading tsconfig.json. --- cli/compiler.rs | 23 +++++ cli/flags.rs | 22 +++++ cli/msg.fbs | 11 +++ cli/ops.rs | 37 ++++++++ cli/state.rs | 40 +++++++++ js/compiler.ts | 169 +++++++++++++++++++++++++++++++++++-- rollup.config.js | 2 + tests/config.test | 4 + tests/config.ts | 5 ++ tests/config.ts.out | 9 ++ tests/config.tsconfig.json | 7 ++ 11 files changed, 322 insertions(+), 7 deletions(-) create mode 100644 tests/config.test create mode 100644 tests/config.ts create mode 100644 tests/config.ts.out create mode 100644 tests/config.tsconfig.json diff --git a/cli/compiler.rs b/cli/compiler.rs index d327835d33fbdb..e9ed8a1d3edf29 100644 --- a/cli/compiler.rs +++ b/cli/compiler.rs @@ -158,6 +158,21 @@ fn req(specifier: &str, referrer: &str, cmd_id: u32) -> Buf { .into_boxed_bytes() } +/// Returns an optional tuple which represents the state of the compiler +/// configuration where the first is canonical name for the configuration file +/// and a vector of the bytes of the contents of the configuration file. +pub fn get_compiler_config( + parent_state: &ThreadSafeState, + _compiler_type: &str, +) -> Option<(String, Vec)> { + match (&parent_state.config_file_name, &parent_state.config) { + (Some(config_file_name), Some(config)) => { + Some((config_file_name.to_string(), config.to_vec())) + } + _ => None, + } +} + pub fn compile_async( parent_state: ThreadSafeState, specifier: &str, @@ -306,4 +321,12 @@ mod tests { assert_eq!(parse_cmd_id(res_json), cmd_id); } + + #[test] + fn test_get_compiler_config_no_flag() { + let compiler_type = "typescript"; + let state = ThreadSafeState::mock(); + let out = get_compiler_config(&state, compiler_type); + assert_eq!(out, None); + } } diff --git a/cli/flags.rs b/cli/flags.rs index 76dbae30b8e74a..fa8905fe228146 100644 --- a/cli/flags.rs +++ b/cli/flags.rs @@ -13,6 +13,7 @@ pub struct DenoFlags { pub log_debug: bool, pub version: bool, pub reload: bool, + pub config: Option, pub allow_read: bool, pub allow_write: bool, pub allow_net: bool, @@ -79,6 +80,13 @@ pub fn create_cli_app<'a, 'b>() -> App<'a, 'b> { .short("r") .long("reload") .help("Reload source code cache (recompile TypeScript)"), + ).arg( + Arg::with_name("config") + .short("c") + .long("config") + .value_name("FILE") + .help("Load compiler configuration file") + .takes_value(true), ).arg( Arg::with_name("v8-options") .long("v8-options") @@ -143,6 +151,7 @@ pub fn parse_flags(matches: ArgMatches) -> DenoFlags { if matches.is_present("reload") { flags.reload = true; } + flags.config = matches.value_of("config").map(ToOwned::to_owned); if matches.is_present("allow-read") { flags.allow_read = true; } @@ -350,4 +359,17 @@ mod tests { } ) } + + #[test] + fn test_set_flags_11() { + let flags = + flags_from_vec(svec!["deno", "-c", "tsconfig.json", "script.ts"]); + assert_eq!( + flags, + DenoFlags { + config: Some("tsconfig.json".to_owned()), + ..DenoFlags::default() + } + ) + } } diff --git a/cli/msg.fbs b/cli/msg.fbs index d217fc7ba70f09..ff5454a9169acb 100644 --- a/cli/msg.fbs +++ b/cli/msg.fbs @@ -3,6 +3,8 @@ union Any { Chdir, Chmod, Close, + CompilerConfig, + CompilerConfigRes, CopyFile, Cwd, CwdRes, @@ -174,6 +176,15 @@ table StartRes { no_color: bool; } +table CompilerConfig { + compiler_type: string; +} + +table CompilerConfigRes { + path: string; + data: [ubyte]; +} + table FormatError { error: string; } diff --git a/cli/ops.rs b/cli/ops.rs index bc06a2fb71a74c..ceafc2a1ae707c 100644 --- a/cli/ops.rs +++ b/cli/ops.rs @@ -1,6 +1,7 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. use atty; use crate::ansi; +use crate::compiler::get_compiler_config; use crate::errors; use crate::errors::{DenoError, DenoResult, ErrorKind}; use crate::fs as deno_fs; @@ -146,6 +147,8 @@ pub fn dispatch_all( pub fn op_selector_compiler(inner_type: msg::Any) -> Option { match inner_type { + msg::Any::CompilerConfig => Some(op_compiler_config), + msg::Any::Cwd => Some(op_cwd), msg::Any::FetchModuleMetaData => Some(op_fetch_module_meta_data), msg::Any::WorkerGetMessage => Some(op_worker_get_message), msg::Any::WorkerPostMessage => Some(op_worker_post_message), @@ -443,6 +446,40 @@ fn op_fetch_module_meta_data( }())) } +fn op_compiler_config( + state: &ThreadSafeState, + base: &msg::Base<'_>, + data: deno_buf, +) -> Box { + assert_eq!(data.len(), 0); + let inner = base.inner_as_compiler_config().unwrap(); + let cmd_id = base.cmd_id(); + let compiler_type = inner.compiler_type().unwrap(); + + Box::new(futures::future::result(|| -> OpResult { + let builder = &mut FlatBufferBuilder::new(); + let (path, out) = match get_compiler_config(state, compiler_type) { + Some(val) => val, + _ => ("".to_owned(), "".as_bytes().to_owned()), + }; + let data_off = builder.create_vector(&out); + let msg_args = msg::CompilerConfigResArgs { + path: Some(builder.create_string(&path)), + data: Some(data_off), + }; + let inner = msg::CompilerConfigRes::create(builder, &msg_args); + Ok(serialize_response( + cmd_id, + builder, + msg::BaseArgs { + inner: Some(inner.as_union_value()), + inner_type: msg::Any::CompilerConfigRes, + ..Default::default() + }, + )) + }())) +} + fn op_chdir( _state: &ThreadSafeState, base: &msg::Base<'_>, diff --git a/cli/state.rs b/cli/state.rs index f10f3b7e0b1d56..3a8a5a1a8d9bc1 100644 --- a/cli/state.rs +++ b/cli/state.rs @@ -15,6 +15,7 @@ use futures::future::Shared; use std; use std::collections::HashMap; use std::env; +use std::fs; use std::ops::Deref; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; @@ -51,6 +52,8 @@ pub struct State { pub argv: Vec, pub permissions: DenoPermissions, pub flags: flags::DenoFlags, + pub config: Option>, + pub config_file_name: Option, pub metrics: Metrics, pub worker_channels: Mutex, pub global_timer: Mutex, @@ -97,11 +100,48 @@ impl ThreadSafeState { let external_channels = (worker_in_tx, worker_out_rx); let resource = resources::add_worker(external_channels); + let config_file = match &flags.config { + 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, + }; + + let config_file_name = match &config_file { + Some(config_file) => Some( + config_file + .canonicalize() + .unwrap() + .to_str() + .unwrap() + .to_owned(), + ), + _ => None, + }; + + let config = match &config_file { + Some(config_file) => { + debug!("Attempt to load config: {}", config_file.to_str().unwrap()); + match fs::read(&config_file) { + Ok(config_data) => Some(config_data.to_owned()), + _ => panic!( + "Error retrieving compiler config file at \"{}\"", + config_file.to_str().unwrap() + ), + } + } + _ => None, + }; + ThreadSafeState(Arc::new(State { dir: deno_dir::DenoDir::new(custom_root).unwrap(), argv: argv_rest, permissions: DenoPermissions::from_flags(&flags), flags, + config, + config_file_name, metrics: Metrics::default(), worker_channels: Mutex::new(internal_channels), global_timer: Mutex::new(GlobalTimer::new()), diff --git a/js/compiler.ts b/js/compiler.ts index fffb9e852d5189..96276879350125 100644 --- a/js/compiler.ts +++ b/js/compiler.ts @@ -5,6 +5,9 @@ import { window } from "./window"; import { assetSourceCode } from "./assets"; import { Console } from "./console"; import { core } from "./core"; +import { cwd } from "./dir"; +import { sendSync } from "./dispatch"; +import * as flatbuffers from "./flatbuffers"; import * as os from "./os"; import { TextDecoder, TextEncoder } from "./text_encoding"; import { clearTimer, setTimeout } from "./timers"; @@ -55,17 +58,81 @@ interface CompilerLookup { interface Os { fetchModuleMetaData: typeof os.fetchModuleMetaData; exit: typeof os.exit; + noColor: typeof os.noColor; } /** Abstraction of the APIs required from the `typescript` module so they can * be easily mocked. */ interface Ts { + convertCompilerOptionsFromJson: typeof ts.convertCompilerOptionsFromJson; createLanguageService: typeof ts.createLanguageService; formatDiagnosticsWithColorAndContext: typeof ts.formatDiagnosticsWithColorAndContext; formatDiagnostics: typeof ts.formatDiagnostics; + parseConfigFileTextToJson: typeof ts.parseConfigFileTextToJson; } +/** Options that either do nothing in Deno, or would cause undesired behavior + * if modified. */ +const ignoredCompilerOptions: ReadonlyArray = [ + "allowSyntheticDefaultImports", + "baseUrl", + "build", + "composite", + "declaration", + "declarationDir", + "declarationMap", + "diagnostics", + "downlevelIteration", + "emitBOM", + "emitDeclarationOnly", + "esModuleInterop", + "extendedDiagnostics", + "forceConsistentCasingInFileNames", + "help", + "importHelpers", + "incremental", + "inlineSourceMap", + "inlineSources", + "init", + "isolatedModules", + "lib", + "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" +]; + /** A simple object structure for caching resolved modules and their contents. * * Named `ModuleMetaData` to clarify it is just a representation of meta data of @@ -201,6 +268,18 @@ class Compiler implements ts.LanguageServiceHost, ts.FormatDiagnosticsHost { ); } + /** Log TypeScript diagnostics to the console and exit */ + private _logDiagnostics(diagnostics: ts.Diagnostic[]): never { + const errMsg = this._os.noColor + ? this._ts.formatDiagnostics(diagnostics, this) + : this._ts.formatDiagnosticsWithColorAndContext(diagnostics, this); + + console.log(errMsg); + // TODO The compiler isolate shouldn't exit. Errors should be forwarded to + // to the caller and the caller exit. + return this._os.exit(1); + } + /** Given a `moduleSpecifier` and `containingFile` retrieve the cached * `fileName` for a given module. If the module has yet to be resolved * this will return `undefined`. @@ -354,13 +433,7 @@ class Compiler implements ts.LanguageServiceHost, ts.FormatDiagnosticsHost { ...service.getSemanticDiagnostics(fileName) ]; if (diagnostics.length > 0) { - const errMsg = os.noColor - ? this._ts.formatDiagnostics(diagnostics, this) - : this._ts.formatDiagnosticsWithColorAndContext(diagnostics, this); - - console.log(errMsg); - // All TypeScript errors are terminal for deno - this._os.exit(1); + this._logDiagnostics(diagnostics); } assert( @@ -392,6 +465,40 @@ class Compiler implements ts.LanguageServiceHost, ts.FormatDiagnosticsHost { return { outputCode, sourceMap }; } + /** Take a configuration string, parse it, and use it to merge with the + * compiler's configuration options. The method returns an array of compiler + * options which were ignored, or `undefined`. + */ + configure(path: string, configurationText: string): string[] | undefined { + this._log("compile.configure", path); + const { config, error } = this._ts.parseConfigFileTextToJson( + path, + configurationText + ); + if (error) { + this._logDiagnostics([error]); + } + const { options, errors } = this._ts.convertCompilerOptionsFromJson( + config.compilerOptions, + cwd() + ); + if (errors.length) { + this._logDiagnostics(errors); + } + const ignoredOptions: string[] = []; + for (const key of Object.keys(options)) { + if ( + ignoredCompilerOptions.includes(key) && + (!(key in this._options) || options[key] !== this._options[key]) + ) { + ignoredOptions.push(key); + delete options[key]; + } + } + Object.assign(this._options, options); + return ignoredOptions.length ? ignoredOptions : undefined; + } + // TypeScript Language Service and Format Diagnostic Host API getCanonicalFileName(fileName: string): string { @@ -541,6 +648,54 @@ window.compilerMain = function compilerMain(): void { }; }; +const decoder = new TextDecoder(); + +// Perform the op to retrieve the compiler configuration if there was any +// provided on startup. +function getCompilerConfig( + compilerType: string +): { path: string; data: string } { + const builder = flatbuffers.createBuilder(); + const compilerType_ = builder.createString(compilerType); + msg.CompilerConfig.startCompilerConfig(builder); + msg.CompilerConfig.addCompilerType(builder, compilerType_); + const inner = msg.CompilerConfig.endCompilerConfig(builder); + const baseRes = sendSync(builder, msg.Any.CompilerConfig, inner); + assert(baseRes != null); + assert(msg.Any.CompilerConfigRes === baseRes!.innerType()); + const res = new msg.CompilerConfigRes(); + assert(baseRes!.inner(res) != null); + + // the privileged side does not normalize path separators in windows, so we + // will normalize them here + const path = res.path()!.replace(/\\/g, "/"); + assert(path != null); + const dataArray = res.dataArray()!; + assert(dataArray != null); + const data = decoder.decode(dataArray); + return { path, data }; +} + export default function denoMain(): void { os.start("TS"); + + const { path, data } = getCompilerConfig("typescript"); + if (data.length) { + const ignoredOptions = compiler.configure(path, data); + if (ignoredOptions) { + if (os.noColor) { + console.warn( + `Unsupported compiler options in "${path}"\n` + + ` The following options were ignored:\n` + + ` ${ignoredOptions.join(", ")}` + ); + } else { + console.warn( + `\x1b[33mUnsupported compiler options in "${path}"\x1b[39m\n` + + ` \x1b[36mThe following options were ignored:\x1b[39m\n` + + ` \x1b[1m${ignoredOptions.join("\x1b[22m, \x1b[1m")}\x1b[22m` + ); + } + } + } } diff --git a/rollup.config.js b/rollup.config.js index 0907ba737184d6..31ee1dc23fd040 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -229,9 +229,11 @@ export default function makeConfig(commandOptions) { // rollup requires them to be explicitly defined to make them available in the // bundle [typescriptPath]: [ + "convertCompilerOptionsFromJson", "createLanguageService", "formatDiagnostics", "formatDiagnosticsWithColorAndContext", + "parseConfigFileTextToJson", "version", "Extension", "ModuleKind", diff --git a/tests/config.test b/tests/config.test new file mode 100644 index 00000000000000..ee811ab8b1cc13 --- /dev/null +++ b/tests/config.test @@ -0,0 +1,4 @@ +args: --reload --config tests/config.tsconfig.json tests/config.ts +check_stderr: true +exit_code: 1 +output: tests/config.ts.out diff --git a/tests/config.ts b/tests/config.ts new file mode 100644 index 00000000000000..e08061e774d6bc --- /dev/null +++ b/tests/config.ts @@ -0,0 +1,5 @@ +const map = new Map(); + +if (map.get("bar").foo) { + console.log("here"); +} diff --git a/tests/config.ts.out b/tests/config.ts.out new file mode 100644 index 00000000000000..e3ceb52bc8d17a --- /dev/null +++ b/tests/config.ts.out @@ -0,0 +1,9 @@ +Unsupported compiler options in "[WILDCARD]tests/config.tsconfig.json" + The following options were ignored: + module, target +Compiling file://[WILDCARD]tests/config.ts +[WILDCARD]tests/config.ts:3:5 - error TS2532: Object is possibly 'undefined'. + +3 if (map.get("bar").foo) { + ~~~~~~~~~~~~~~ + diff --git a/tests/config.tsconfig.json b/tests/config.tsconfig.json new file mode 100644 index 00000000000000..074d7ac0bc1929 --- /dev/null +++ b/tests/config.tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "module": "amd", + "strict": true, + "target": "es5" + } +}