1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

refactor: Compiler config in Rust (#7228)

* port tsc_config.rs

* cleanup options

* bring back allowNonTsExtension

* try

* fix test

* fix test2

* move config for bundling

* remove Transpile compile request

* remove dead code

* remove more dead code

* remove checkJs regex

* fix

* handle config str for runtime APIs

* lint

* runtimeCompile config in Rust

* runtimeCompile and runtimeTranspile config in Rust

* fix

* remove lint supression

* upgrade: jsonc-parser 0.13.0

* remove unneeded to_string()

* upgrade: jsonc-parser 0.14.0

* remove AsRef<str>
This commit is contained in:
Bartek Iwańczuk 2020-08-31 20:12:24 +02:00 committed by GitHub
parent 71f0171ab0
commit c82c3b982e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 653 additions and 511 deletions

7
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -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<tsc_config::IgnoredCompilerOptions>,
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<PathBuf>,
pub content: Option<Vec<u8>>,
pub hash: Vec<u8>,
pub options: Value,
pub maybe_ignored_options: Option<tsc_config::IgnoredCompilerOptions>,
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<String>) -> Result<Self, ErrBox> {
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<String>) -> Result<Self, ErrBox> {
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<HashMap<String, String>>,
maybe_options: &Option<String>,
type_files: Vec<String>,
) -> Result<(Vec<String>, 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<HashMap<String, String>>,
maybe_options: &Option<String>,
) -> Result<Value, ErrBox> {
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::<Vec<String>>();
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<HashMap<String, String>>,
maybe_options: &Option<String>,
) -> Result<Value, ErrBox> {
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::<Vec<String>>();
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<GlobalState>,
permissions: Permissions,
sources: &HashMap<String, String>,
options: &Option<String>,
maybe_options: &Option<String>,
) -> Result<Value, ErrBox> {
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),
];

View file

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

236
cli/tsc_config.rs Normal file
View file

@ -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<String>);
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<HashMap<String, Value>>,
exclude: Option<Vec<String>>,
extends: Option<String>,
files: Option<Vec<String>>,
include: Option<Vec<String>>,
references: Option<Value>,
type_acquisition: Option<Value>,
}
pub fn parse_raw_config(config_text: &str) -> Result<Value, ErrBox> {
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<IgnoredCompilerOptions>), 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<String, Value> = HashMap::new();
let mut items: Vec<String> = 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"));
}
}