mirror of
https://github.com/denoland/deno.git
synced 2024-11-26 16:09:27 -05:00
b0482400c9
Fixes #8163
428 lines
11 KiB
Rust
428 lines
11 KiB
Rust
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
|
|
|
use deno_core::error::AnyError;
|
|
use deno_core::serde_json;
|
|
use deno_core::serde_json::json;
|
|
use deno_core::serde_json::Value;
|
|
use jsonc_parser::JsonValue;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use serde::Serializer;
|
|
use std::collections::BTreeMap;
|
|
use std::collections::HashMap;
|
|
use std::fmt;
|
|
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
use std::str::FromStr;
|
|
|
|
/// The transpile options that are significant out of a user provided tsconfig
|
|
/// file, that we want to deserialize out of the final config for a transpile.
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct EmitConfigOptions {
|
|
pub check_js: bool,
|
|
pub emit_decorator_metadata: bool,
|
|
pub inline_source_map: bool,
|
|
pub jsx: String,
|
|
pub jsx_factory: String,
|
|
pub jsx_fragment_factory: String,
|
|
}
|
|
|
|
/// A structure that represents a set of options that were ignored and the
|
|
/// path those options came from.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct IgnoredCompilerOptions {
|
|
pub items: Vec<String>,
|
|
pub maybe_path: Option<PathBuf>,
|
|
}
|
|
|
|
impl fmt::Display for IgnoredCompilerOptions {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let mut codes = self.items.clone();
|
|
codes.sort();
|
|
if let Some(path) = &self.maybe_path {
|
|
write!(f, "Unsupported compiler options in \"{}\".\n The following options were ignored:\n {}", path.to_string_lossy(), codes.join(", "))
|
|
} else {
|
|
write!(f, "Unsupported compiler options provided.\n The following options were ignored:\n {}", codes.join(", "))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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; 10] = [
|
|
"allowSyntheticDefaultImports",
|
|
"esModuleInterop",
|
|
"inlineSourceMap",
|
|
"inlineSources",
|
|
// TODO(nayeemrmn): Add "isolatedModules" here for 1.6.0.
|
|
"module",
|
|
"noLib",
|
|
"preserveConstEnums",
|
|
"reactNamespace",
|
|
"sourceMap",
|
|
"target",
|
|
];
|
|
|
|
const IGNORED_RUNTIME_COMPILER_OPTIONS: [&str; 50] = [
|
|
"allowUmdGlobalAccess",
|
|
"assumeChangesOnlyAffectDirectDependencies",
|
|
"baseUrl",
|
|
"build",
|
|
"composite",
|
|
"declaration",
|
|
"declarationMap",
|
|
"diagnostics",
|
|
"downlevelIteration",
|
|
"emitBOM",
|
|
"emitDeclarationOnly",
|
|
"extendedDiagnostics",
|
|
"forceConsistentCasingInFileNames",
|
|
"generateCpuProfile",
|
|
"help",
|
|
"importHelpers",
|
|
"incremental",
|
|
"init",
|
|
"listEmittedFiles",
|
|
"listFiles",
|
|
"mapRoot",
|
|
"maxNodeModuleJsDepth",
|
|
"moduleResolution",
|
|
"newLine",
|
|
"noEmit",
|
|
"noEmitHelpers",
|
|
"noEmitOnError",
|
|
"noResolve",
|
|
"out",
|
|
"outDir",
|
|
"outFile",
|
|
"paths",
|
|
"preserveSymlinks",
|
|
"preserveWatchOutput",
|
|
"pretty",
|
|
"resolveJsonModule",
|
|
"rootDir",
|
|
"rootDirs",
|
|
"showConfig",
|
|
"skipDefaultLibCheck",
|
|
"skipLibCheck",
|
|
"sourceRoot",
|
|
"stripInternal",
|
|
"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, AnyError> {
|
|
assert!(!config_text.is_empty());
|
|
let jsonc = jsonc_parser::parse_to_value(config_text)?.unwrap();
|
|
Ok(jsonc_to_serde(jsonc))
|
|
}
|
|
|
|
fn parse_compiler_options(
|
|
compiler_options: &HashMap<String, Value>,
|
|
maybe_path: Option<PathBuf>,
|
|
is_runtime: bool,
|
|
) -> Result<(Value, Option<IgnoredCompilerOptions>), AnyError> {
|
|
let mut filtered: HashMap<String, Value> = HashMap::new();
|
|
let mut items: Vec<String> = Vec::new();
|
|
|
|
for (key, value) in compiler_options.iter() {
|
|
let key = key.as_str();
|
|
if (!is_runtime && IGNORED_COMPILER_OPTIONS.contains(&key))
|
|
|| IGNORED_RUNTIME_COMPILER_OPTIONS.contains(&key)
|
|
{
|
|
items.push(key.to_string());
|
|
} else {
|
|
filtered.insert(key.to_string(), value.to_owned());
|
|
}
|
|
}
|
|
let value = serde_json::to_value(filtered)?;
|
|
let maybe_ignored_options = if !items.is_empty() {
|
|
Some(IgnoredCompilerOptions { items, maybe_path })
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Ok((value, maybe_ignored_options))
|
|
}
|
|
|
|
/// 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,
|
|
path: &Path,
|
|
) -> Result<(Value, Option<IgnoredCompilerOptions>), AnyError> {
|
|
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))?;
|
|
|
|
if let Some(compiler_options) = config.compiler_options {
|
|
parse_compiler_options(&compiler_options, Some(path.to_owned()), false)
|
|
} else {
|
|
Ok((json!({}), None))
|
|
}
|
|
}
|
|
|
|
/// A structure for managing the configuration of TypeScript
|
|
#[derive(Debug, Clone)]
|
|
pub struct TsConfig(pub Value);
|
|
|
|
impl TsConfig {
|
|
/// Create a new `TsConfig` with the base being the `value` supplied.
|
|
pub fn new(value: Value) -> Self {
|
|
TsConfig(value)
|
|
}
|
|
|
|
pub fn as_bytes(&self) -> Vec<u8> {
|
|
let map = self.0.as_object().unwrap();
|
|
let ordered: BTreeMap<_, _> = map.iter().collect();
|
|
let value = json!(ordered);
|
|
value.to_string().as_bytes().to_owned()
|
|
}
|
|
|
|
/// Return the value of the `checkJs` compiler option, defaulting to `false`
|
|
/// if not present.
|
|
pub fn get_check_js(&self) -> bool {
|
|
if let Some(check_js) = self.0.get("checkJs") {
|
|
check_js.as_bool().unwrap_or(false)
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// Merge a serde_json value into the configuration.
|
|
pub fn merge(&mut self, value: &Value) {
|
|
json_merge(&mut self.0, value);
|
|
}
|
|
|
|
/// Take an optional string representing a user provided TypeScript config file
|
|
/// which was passed in via the `--config` compiler option and merge it with
|
|
/// the configuration. Returning the result which optionally contains any
|
|
/// compiler options that were ignored.
|
|
///
|
|
/// When there are options ignored out of the file, a warning will be written
|
|
/// to stderr regarding the options that were ignored.
|
|
pub fn merge_tsconfig(
|
|
&mut self,
|
|
maybe_path: Option<String>,
|
|
) -> Result<Option<IgnoredCompilerOptions>, AnyError> {
|
|
if let Some(path) = maybe_path {
|
|
let cwd = std::env::current_dir()?;
|
|
let config_file = cwd.join(path);
|
|
let config_path = config_file.canonicalize().map_err(|_| {
|
|
std::io::Error::new(
|
|
std::io::ErrorKind::InvalidInput,
|
|
format!(
|
|
"Could not find the config file: {}",
|
|
config_file.to_string_lossy()
|
|
),
|
|
)
|
|
})?;
|
|
let config_text = std::fs::read_to_string(config_path.clone())?;
|
|
let (value, maybe_ignored_options) =
|
|
parse_config(&config_text, &config_path)?;
|
|
json_merge(&mut self.0, &value);
|
|
|
|
Ok(maybe_ignored_options)
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
/// Take a map of compiler options, filtering out any that are ignored, then
|
|
/// merge it with the current configuration, returning any options that might
|
|
/// have been ignored.
|
|
pub fn merge_user_config(
|
|
&mut self,
|
|
user_options: &HashMap<String, Value>,
|
|
) -> Result<Option<IgnoredCompilerOptions>, AnyError> {
|
|
let (value, maybe_ignored_options) =
|
|
parse_compiler_options(user_options, None, true)?;
|
|
json_merge(&mut self.0, &value);
|
|
Ok(maybe_ignored_options)
|
|
}
|
|
}
|
|
|
|
impl Serialize for TsConfig {
|
|
/// Serializes inner hash map which is ordered by the key
|
|
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
|
where
|
|
S: Serializer,
|
|
{
|
|
Serialize::serialize(&self.0, serializer)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use deno_core::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 config_path = PathBuf::from("/deno/tsconfig.json");
|
|
let (options_value, ignored) =
|
|
parse_config(config_text, &config_path).expect("error parsing");
|
|
assert!(options_value.is_object());
|
|
let options = options_value.as_object().unwrap();
|
|
assert!(options.contains_key("strict"));
|
|
assert_eq!(options.len(), 1);
|
|
assert_eq!(
|
|
ignored,
|
|
Some(IgnoredCompilerOptions {
|
|
items: vec!["build".to_string()],
|
|
maybe_path: Some(config_path),
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_tsconfig_merge_user_options() {
|
|
let mut tsconfig = TsConfig::new(json!({
|
|
"target": "esnext",
|
|
"module": "esnext",
|
|
}));
|
|
let user_options = serde_json::from_value(json!({
|
|
"target": "es6",
|
|
"build": true,
|
|
"strict": false,
|
|
}))
|
|
.expect("could not convert to hashmap");
|
|
let maybe_ignored_options = tsconfig
|
|
.merge_user_config(&user_options)
|
|
.expect("could not merge options");
|
|
assert_eq!(
|
|
tsconfig.0,
|
|
json!({
|
|
"module": "esnext",
|
|
"target": "es6",
|
|
"strict": false,
|
|
})
|
|
);
|
|
assert_eq!(
|
|
maybe_ignored_options,
|
|
Some(IgnoredCompilerOptions {
|
|
items: vec!["build".to_string()],
|
|
maybe_path: None
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_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"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_tsconfig_as_bytes() {
|
|
let mut tsconfig1 = TsConfig::new(json!({
|
|
"strict": true,
|
|
"target": "esnext",
|
|
}));
|
|
tsconfig1.merge(&json!({
|
|
"target": "es5",
|
|
"module": "amd",
|
|
}));
|
|
let mut tsconfig2 = TsConfig::new(json!({
|
|
"target": "esnext",
|
|
"strict": true,
|
|
}));
|
|
tsconfig2.merge(&json!({
|
|
"module": "amd",
|
|
"target": "es5",
|
|
}));
|
|
assert_eq!(tsconfig1.as_bytes(), tsconfig2.as_bytes());
|
|
}
|
|
}
|