Add support for custom tsconfig.json (#2089)

Use `--config`
This commit is contained in:
Kitson Kelly 2019-04-29 15:58:31 +01:00 committed by Ryan Dahl
parent 73be183864
commit 1a0f53a807
14 changed files with 438 additions and 25 deletions

View File

@ -52,9 +52,10 @@ ts_sources = [
"../js/buffer.ts",
"../js/build.ts",
"../js/chmod.ts",
"../js/console_table.ts",
"../js/colors.ts",
"../js/compiler.ts",
"../js/console.ts",
"../js/console_table.ts",
"../js/copy_file.ts",
"../js/core.ts",
"../js/custom_event.ts",

View File

@ -158,6 +158,23 @@ 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<u8>)> {
// The compiler type is being passed to make it easier to implement custom
// compilers in the future.
match (&parent_state.config_path, &parent_state.config) {
(Some(config_path), Some(config)) => {
Some((config_path.to_string(), config.to_vec()))
}
_ => None,
}
}
pub fn compile_async(
parent_state: ThreadSafeState,
specifier: &str,
@ -306,4 +323,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);
}
}

View File

@ -51,12 +51,18 @@ pub struct DenoDir {
// This splits to http and https deps
pub deps_http: PathBuf,
pub deps_https: PathBuf,
/// The active configuration file contents (or empty array) which applies to
/// source code cached by `DenoDir`.
pub config: Vec<u8>,
}
impl DenoDir {
// Must be called before using any function from this module.
// https://github.com/denoland/deno/blob/golang/deno_dir.go#L99-L111
pub fn new(custom_root: Option<PathBuf>) -> std::io::Result<Self> {
pub fn new(
custom_root: Option<PathBuf>,
state_config: &Option<Vec<u8>>,
) -> std::io::Result<Self> {
// Only setup once.
let home_dir = dirs::home_dir().expect("Could not get home directory.");
let fallback = home_dir.join(".deno");
@ -73,12 +79,22 @@ impl DenoDir {
let deps_http = deps.join("http");
let deps_https = deps.join("https");
// Internally within DenoDir, we use the config as part of the hash to
// determine if a file has been transpiled with the same configuration, but
// we have borrowed the `State` configuration, which we want to either clone
// or create an empty `Vec` which we will use in our hash function.
let config = match state_config {
Some(config) => config.clone(),
_ => b"".to_vec(),
};
let deno_dir = Self {
root,
gen,
deps,
deps_http,
deps_https,
config,
};
// TODO Lazily create these directories.
@ -102,7 +118,8 @@ impl DenoDir {
filename: &str,
source_code: &[u8],
) -> (PathBuf, PathBuf) {
let cache_key = source_code_hash(filename, source_code, version::DENO);
let cache_key =
source_code_hash(filename, source_code, version::DENO, &self.config);
(
self.gen.join(cache_key.to_string() + ".js"),
self.gen.join(cache_key.to_string() + ".js.map"),
@ -156,6 +173,11 @@ impl DenoDir {
let gen = self.gen.clone();
// If we don't clone the config, we then end up creating an implied lifetime
// which gets returned in the future, so we clone here so as to not leak the
// move below when the future is resolving.
let config = self.config.clone();
Either::B(
get_source_code_async(
self,
@ -191,8 +213,12 @@ impl DenoDir {
return Ok(out);
}
let cache_key =
source_code_hash(&out.filename, &out.source_code, version::DENO);
let cache_key = source_code_hash(
&out.filename,
&out.source_code,
version::DENO,
&config,
);
let (output_code_filename, output_source_map_filename) = (
gen.join(cache_key.to_string() + ".js"),
gen.join(cache_key.to_string() + ".js.map"),
@ -468,15 +494,19 @@ fn load_cache2(
Ok((read_output_code, read_source_map))
}
/// Generate an SHA1 hash for source code, to be used to determine if a cached
/// version of the code is valid or invalid.
fn source_code_hash(
filename: &str,
source_code: &[u8],
version: &str,
config: &[u8],
) -> String {
let mut ctx = ring::digest::Context::new(&ring::digest::SHA1);
ctx.update(version.as_bytes());
ctx.update(filename.as_bytes());
ctx.update(source_code);
ctx.update(config);
let digest = ctx.finish();
let mut out = String::new();
// TODO There must be a better way to do this...
@ -860,8 +890,9 @@ mod tests {
fn test_setup() -> (TempDir, DenoDir) {
let temp_dir = TempDir::new().expect("tempdir fail");
let deno_dir =
DenoDir::new(Some(temp_dir.path().to_path_buf())).expect("setup fail");
let config = Some(b"{}".to_vec());
let deno_dir = DenoDir::new(Some(temp_dir.path().to_path_buf()), &config)
.expect("setup fail");
(temp_dir, deno_dir)
}
@ -904,7 +935,8 @@ mod tests {
let (temp_dir, deno_dir) = test_setup();
let filename = "hello.js";
let source_code = b"1+2";
let hash = source_code_hash(filename, source_code, version::DENO);
let config = b"{}";
let hash = source_code_hash(filename, source_code, version::DENO, config);
assert_eq!(
(
temp_dir.path().join(format!("gen/{}.js", hash)),
@ -914,6 +946,24 @@ mod tests {
);
}
#[test]
fn test_cache_path_config() {
// We are changing the compiler config from the "mock" and so we expect the
// resolved files coming back to not match the calculated hash.
let (temp_dir, deno_dir) = test_setup();
let filename = "hello.js";
let source_code = b"1+2";
let config = b"{\"compilerOptions\":{}}";
let hash = source_code_hash(filename, source_code, version::DENO, config);
assert_ne!(
(
temp_dir.path().join(format!("gen/{}.js", hash)),
temp_dir.path().join(format!("gen/{}.js.map", hash))
),
deno_dir.cache_path(filename, source_code)
);
}
#[test]
fn test_code_cache() {
let (_temp_dir, deno_dir) = test_setup();
@ -922,7 +972,8 @@ mod tests {
let source_code = b"1+2";
let output_code = b"1+2 // output code";
let source_map = b"{}";
let hash = source_code_hash(filename, source_code, version::DENO);
let config = b"{}";
let hash = source_code_hash(filename, source_code, version::DENO, config);
let (cache_path, source_map_path) =
deno_dir.cache_path(filename, source_code);
assert!(cache_path.ends_with(format!("gen/{}.js", hash)));
@ -949,23 +1000,23 @@ mod tests {
#[test]
fn test_source_code_hash() {
assert_eq!(
"7e44de2ed9e0065da09d835b76b8d70be503d276",
source_code_hash("hello.ts", b"1+2", "0.2.11")
"830c8b63ba3194cf2108a3054c176b2bf53aee45",
source_code_hash("hello.ts", b"1+2", "0.2.11", b"{}")
);
// Different source_code should result in different hash.
assert_eq!(
"57033366cf9db1ef93deca258cdbcd9ef5f4bde1",
source_code_hash("hello.ts", b"1", "0.2.11")
"fb06127e9b2e169bea9c697fa73386ae7c901e8b",
source_code_hash("hello.ts", b"1", "0.2.11", b"{}")
);
// Different filename should result in different hash.
assert_eq!(
"19657f90b5b0540f87679e2fb362e7bd62b644b0",
source_code_hash("hi.ts", b"1+2", "0.2.11")
"3a17b6a493ff744b6a455071935f4bdcd2b72ec7",
source_code_hash("hi.ts", b"1+2", "0.2.11", b"{}")
);
// Different version should result in different hash.
assert_eq!(
"e2b4b7162975a02bf2770f16836eb21d5bcb8be1",
source_code_hash("hi.ts", b"1+2", "0.2.0")
"d6b2cfdc39dae9bd3ad5b493ee1544eb22e7475f",
source_code_hash("hi.ts", b"1+2", "0.2.0", b"{}")
);
}

View File

@ -13,6 +13,9 @@ pub struct DenoFlags {
pub log_debug: bool,
pub version: bool,
pub reload: bool,
/// When the `--config`/`-c` flag is used to pass the name, this will be set
/// the path passed on the command line, otherwise `None`.
pub config_path: Option<String>,
pub allow_read: bool,
pub allow_write: bool,
pub allow_net: bool,
@ -79,6 +82,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")
@ -146,6 +156,7 @@ pub fn parse_flags(matches: ArgMatches) -> DenoFlags {
if matches.is_present("reload") {
flags.reload = true;
}
flags.config_path = matches.value_of("config").map(ToOwned::to_owned);
if matches.is_present("allow-read") {
flags.allow_read = true;
}
@ -353,4 +364,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_path: Some("tsconfig.json".to_owned()),
..DenoFlags::default()
}
)
}
}

View File

@ -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;
}

View File

@ -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<OpCreator> {
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,41 @@ fn op_fetch_module_meta_data(
}()))
}
/// Retrieve any relevant compiler configuration.
fn op_compiler_config(
state: &ThreadSafeState,
base: &msg::Base<'_>,
data: deno_buf,
) -> Box<OpWithError> {
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<'_>,

View File

@ -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,12 @@ pub struct State {
pub argv: Vec<String>,
pub permissions: DenoPermissions,
pub flags: flags::DenoFlags,
/// When flags contains a `.config_path` option, the content of the
/// configuration file will be resolved and set.
pub config: Option<Vec<u8>>,
/// When flags contains a `.config_path` option, the fully qualified path
/// name of the passed path will be resolved and set.
pub config_path: Option<String>,
pub metrics: Metrics,
pub worker_channels: Mutex<WorkerChannels>,
pub global_timer: Mutex<GlobalTimer>,
@ -97,11 +104,52 @@ impl ThreadSafeState {
let external_channels = (worker_in_tx, worker_out_rx);
let resource = resources::add_worker(external_channels);
// take the passed flag and resolve the file name relative to the cwd
let config_file = match &flags.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,
};
// 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()
.unwrap()
.to_str()
.unwrap()
.to_owned(),
),
_ => None,
};
// 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());
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(),
dir: deno_dir::DenoDir::new(custom_root, &config).unwrap(),
argv: argv_rest,
permissions: DenoPermissions::from_flags(&flags),
flags,
config,
config_path,
metrics: Metrics::default(),
worker_channels: Mutex::new(internal_channels),
global_timer: Mutex::new(GlobalTimer::new()),

40
js/colors.ts Normal file
View File

@ -0,0 +1,40 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
// TODO(kitsonk) Replace with `deno_std/colors/mod.ts` when we can load modules
// which end in `.ts`.
import { noColor } from "./os";
interface Code {
open: string;
close: string;
regexp: RegExp;
}
let enabled = !noColor;
function code(open: number, close: number): Code {
return {
open: `\x1b[${open}m`,
close: `\x1b[${close}m`,
regexp: new RegExp(`\\x1b\\[${close}m`, "g")
};
}
function run(str: string, code: Code): string {
return enabled
? `${code.open}${str.replace(code.regexp, code.open)}${code.close}`
: str;
}
export function bold(str: string): string {
return run(str, code(1, 22));
}
export function yellow(str: string): string {
return run(str, code(33, 39));
}
export function cyan(str: string): string {
return run(str, code(36, 39));
}

View File

@ -3,8 +3,12 @@ import * as ts from "typescript";
import * as msg from "gen/cli/msg_generated";
import { window } from "./window";
import { assetSourceCode } from "./assets";
import { bold, cyan, yellow } from "./colors";
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 +59,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<string> = [
"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 +269,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 +434,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 +466,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 +649,46 @@ 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) {
console.warn(
yellow(`Unsupported compiler options in "${path}"\n`) +
cyan(` The following options were ignored:\n`) +
` ${ignoredOptions.map((value): string => bold(value)).join(", ")}`
);
}
}
}

View File

@ -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",

4
tests/config.test Normal file
View File

@ -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

5
tests/config.ts Normal file
View File

@ -0,0 +1,5 @@
const map = new Map<string, { foo: string }>();
if (map.get("bar").foo) {
console.log("here");
}

9
tests/config.ts.out Normal file
View File

@ -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) {
~~~~~~~~~~~~~~

View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"module": "amd",
"strict": true,
"target": "es5"
}
}