tsconfig/
lib.rs

1//! A Rust crate for parsing TypeScript's TSConfig files into a struct.
2//!
3//! A TSConfig file in a directory indicates that the directory is the root of a TypeScript or JavaScript project.
4//! The TSConfig file can be either a tsconfig.json or jsconfig.json; both have the same behavior and the same set of config variables.
5//!
6//! One TSConfig can inherit fields from another if it is specified in the 'extends' field.
7//!
8//! ## Example usage
9//!
10//! ```
11//! use tsconfig::TsConfig;
12//! use std::path::Path;
13//!
14//! let path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
15//!     .join("test/tsconfig.default.json");
16//! let config = TsConfig::parse_file(&path).unwrap();
17//!
18//! ```
19
20use std::path::Path;
21use std::{collections::HashMap, io::Read};
22
23use json_comments::StripComments;
24use regex::Regex;
25use serde::{Deserialize, Deserializer};
26use serde_json::Value;
27
28use thiserror::Error;
29
30pub type Result<T, E = ConfigError> = std::result::Result<T, E>;
31
32/// Errors when parsing TsConfig files.
33/// This is non-exhaustive, and may be extended in the future.
34#[non_exhaustive]
35#[derive(Debug, Error)]
36pub enum ConfigError {
37    #[error("Could not parse configuration file")]
38    ParseError(#[from] serde_json::Error),
39    #[error("Could not read file")]
40    CouldNotFindFile(#[from] std::io::Error),
41    #[error("Could not convert path into UTF-8: {0}")]
42    InvalidPath(String),
43}
44
45/// The main struct representing a parsed .tsconfig file.
46#[derive(Deserialize, Debug, Clone)]
47#[serde(rename_all = "camelCase")]
48pub struct TsConfig {
49    pub exclude: Option<Vec<String>>,
50    pub extends: Option<String>,
51    pub files: Option<Vec<String>>,
52    pub include: Option<Vec<String>>,
53    pub references: Option<References>,
54    pub type_acquisition: Option<TypeAcquisition>,
55    pub compiler_options: Option<CompilerOptions>,
56}
57
58impl TsConfig {
59    /// Parses a .tsconfig file into a [TsConfig].
60    ///
61    /// The `extends` field will be respected, allowing for one .tsconfig file to inherit properties from another.
62    /// Comments and trailing commas are both allowed, although they are not valid JSON.
63    /// ## Example
64    ///
65    /// Assuming the following .tsconfig files:
66    ///
67    /// tsconfig.base.json:
68    /// ```json
69    /// {
70    ///     "useDefineForClassFields": false,
71    ///     "traceResolution": true,
72    ///     "jsx": "preserve",
73    /// }
74    /// ```
75    /// tsconfig.inherits.json:
76    /// ```json
77    /// {
78    ///     "extends": "./tsconfig.base.json",
79    ///     "compilerOptions": {
80    ///         "traceResolution": false,
81    ///         "declaration": true,
82    ///         "jsx": "react-jsxdev",
83    ///     }
84    /// }
85    /// ```
86    /// ```
87    /// use std::path::Path;
88    /// use tsconfig::TsConfig;
89    /// let path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
90    ///     .join("test/tsconfig.inherits.json");
91    /// let config = TsConfig::parse_file(&path).unwrap();
92    ///
93    /// assert_eq!(
94    ///     config
95    ///         .compiler_options
96    ///         .clone()
97    ///         .unwrap()
98    ///         .use_define_for_class_fields,
99    ///     Some(false)
100    /// );
101    ///
102    /// assert_eq!(
103    ///     config.compiler_options.clone().unwrap().declaration,
104    ///     Some(true)
105    /// );
106    ///
107    /// assert_eq!(
108    ///     config.compiler_options.unwrap().trace_resolution,
109    ///     Some(false)
110    /// );
111    ///
112    /// ```
113    pub fn parse_file<P: AsRef<Path>>(path: &P) -> Result<TsConfig> {
114        let values = parse_file_to_value(path)?;
115        let cfg = serde_json::from_value(values)?;
116        Ok(cfg)
117    }
118
119    /// Parse a JSON string into a single [TsConfig].
120    ///
121    /// The 'extends' field will be ignored. Comments and trailing commas are both allowed, although they are not valid JSON.
122    ///
123    /// ## Example
124    /// ```
125    /// use tsconfig::{TsConfig, Jsx};
126    /// let json = r#"{"compilerOptions": {"jsx": /*here's a comment*/ "react-jsx"},}"#;
127    ///
128    /// let config = TsConfig::parse_str(json).unwrap();
129    /// assert_eq!(config.compiler_options.unwrap().jsx, Some(Jsx::ReactJsx));     
130    ///```
131    ///
132    pub fn parse_str(json: &str) -> Result<TsConfig> {
133        // Remove trailing commas from objects.
134        let re = Regex::new(r",(?P<valid>\s*})").unwrap();
135        let mut stripped = String::with_capacity(json.len());
136        StripComments::new(json.as_bytes()).read_to_string(&mut stripped)?;
137        let stripped = re.replace_all(&stripped, "$valid");
138        let r: TsConfig = serde_json::from_str(&stripped)?;
139        Ok(r)
140    }
141}
142
143fn merge(a: &mut Value, b: Value) {
144    match (a, b) {
145        (&mut Value::Object(ref mut a), Value::Object(b)) => {
146            for (k, v) in b {
147                merge(a.entry(k).or_insert(Value::Null), v);
148            }
149        }
150        (a, b) => {
151            if let Value::Null = a {
152                *a = b;
153            }
154        }
155    }
156}
157
158/// Parses a .tsconfig file into a [serde_json::Value].
159///
160/// The `extends` field will be respected, allowing for one .tsconfig file to inherit properties from another.
161/// Comments and trailing commas are both allowed, although they are not valid JSON.
162/// ## Example
163///
164/// Assuming the following .tsconfig files:
165///
166/// tsconfig.base.json:
167/// ```json
168/// {
169///     "compilerOptions": {
170///         "useDefineForClassFields": false,
171///         "traceResolution": true,
172///         "jsx": "preserve",
173///     }
174/// }
175/// ```
176/// tsconfig.inherits.json:
177/// ```json
178/// {
179///     "extends": "./tsconfig.base.json",
180///     "compilerOptions": {
181///         "traceResolution": false,
182///         "declaration": true,
183///         "jsx": "react-jsxdev",
184///     }
185/// }
186/// ```
187/// ```
188/// use std::path::Path;
189/// use tsconfig::parse_file_to_value;
190/// use serde_json::Value;
191///
192/// let path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
193///     .join("test/tsconfig.inherits.json");
194/// let config = parse_file_to_value(&path).unwrap();
195///
196/// assert_eq!(
197///     config
198///         ["compilerOptions"]
199///         ["useDefineForClassFields"],
200///     Value::Bool(false)
201/// );
202///
203///
204/// ```
205pub fn parse_file_to_value<P: AsRef<Path>>(path: &P) -> Result<Value> {
206    let s = std::fs::read_to_string(path)?;
207    let mut value = parse_to_value(&s)?;
208
209    if let Value::String(s) = &value["extends"] {
210        // This may or may not have a `.json` extension
211        let extends_path_unchecked = path
212            .as_ref()
213            .parent()
214            .unwrap_or_else(|| Path::new(""))
215            .join(s);
216
217        let extends_path_str = extends_path_unchecked.to_str().ok_or_else(|| {
218            ConfigError::InvalidPath(extends_path_unchecked.to_string_lossy().to_string())
219        })?;
220
221        // Append the extension if it doesn't already have it
222        let extends_path = if extends_path_str.ends_with(&".json") {
223            extends_path_unchecked
224        } else {
225            let with_ext = extends_path_str.to_string() + ".json";
226            Path::new(with_ext.as_str()).to_path_buf()
227        };
228        let extends_value = parse_file_to_value(&extends_path)?;
229        merge(&mut value, extends_value);
230    }
231
232    Ok(value)
233}
234
235/// Parse a JSON string into a single [serde_json::Value].
236///
237/// The 'extends' field will be ignored. Comments and trailing commas are both allowed, although they are not valid JSON.
238///
239/// ## Example
240/// ```
241/// use tsconfig::parse_to_value;
242/// use serde_json::Value;
243///
244///
245/// let json = r#"{"compilerOptions": {"jsx": /*here's a comment*/ "react-jsx"},}"#;
246///
247/// let config = parse_to_value(json).unwrap();
248/// assert_eq!(config["compilerOptions"]["jsx"], Value::String("react-jsx".to_string()));     
249///```
250///
251pub fn parse_to_value(json: &str) -> Result<Value> {
252    // Remove trailing commas from objects.
253    let re = Regex::new(r",(?P<valid>\s*})").unwrap();
254    let mut stripped = String::with_capacity(json.len());
255    StripComments::new(json.as_bytes()).read_to_string(&mut stripped)?;
256    let stripped = re.replace_all(&stripped, "$valid");
257    let r: Value = serde_json::from_str(&stripped)?;
258    Ok(r)
259}
260
261/// Project references setting  
262///
263/// Project references are a way to structure your TypeScript programs into smaller pieces. Using
264/// Project References can greatly improve build and editor interaction times, enforce logical separation
265/// between components, and organize your code in new and improved ways.
266///
267/// You can read more about how references works in the Project References section of [the handbook](https://www.typescriptlang.org/docs/handbook/project-references.html).
268#[derive(Deserialize, Debug, Clone)]
269#[serde(untagged)]
270pub enum References {
271    Bool(bool),
272    References(Vec<Reference>),
273}
274
275/// Project references setting  
276///
277/// Project references are a way to structure your TypeScript programs into smaller pieces. Using
278/// Project References can greatly improve build and editor interaction times, enforce logical separation
279/// between components, and organize your code in new and improved ways.
280///
281/// You can read more about how references works in the Project References section of [the handbook](https://www.typescriptlang.org/docs/handbook/project-references.html).
282#[derive(Deserialize, Debug, Clone)]
283pub struct Reference {
284    pub path: String,
285    pub prepend: Option<bool>,
286}
287
288/// Defines how automatic type acquisition behaves.
289///
290/// When you have a JavaScript project in your editor, TypeScript will provide types for your node_modules automatically
291/// using the DefinitelyTyped set of @types definitions. This is called automatic type acquisition, and you can customize
292/// it using the typeAcquisition object in your configuration.
293///
294/// If you would like to disable or customize this feature, create a jsconfig.json in the root of your project:
295///
296/// ```json
297/// {
298///   "typeAcquisition": {
299///     "enable": false
300///   }
301/// }
302/// ```
303///
304/// If you have a specific module which should be included (but isn’t in node_modules):
305///
306/// ```json
307/// {
308///   "typeAcquisition": {
309///     "include": ["jest"]
310///   }
311/// }
312/// ```
313///
314/// If a module should not be automatically acquired, for example if the library is available in your node_modules but your team has agreed to not use it:
315///
316/// ```json
317/// {
318///   "typeAcquisition": {
319///     "exclude": ["jquery"]
320///   }
321/// }
322/// ```
323///
324/// In TypeScript 4.1, we added the ability to disable the special-casing where a filename would trigger type acquisition:
325///
326/// ```json
327/// {
328///   "typeAcquisition": {
329///     "disableFilenameBasedTypeAcquisition": true
330///   }
331/// }
332/// ```
333///
334/// This means that having a file like jquery.js in your project would not automatically download the types for JQuery from DefinitelyTyped.
335///
336#[derive(Deserialize, Debug, Clone)]
337pub enum TypeAcquisition {
338    Bool(bool),
339    Object {
340        enable: bool,
341        include: Option<Vec<String>>,
342        exclude: Option<Vec<String>>,
343        disable_filename_based_type_acquisition: Option<bool>,
344    },
345}
346
347/// These options make up the bulk of TypeScript’s configuration and it covers how the language should work.
348#[derive(Deserialize, Debug, Clone)]
349#[serde(rename_all = "camelCase")]
350pub struct CompilerOptions {
351    pub allow_js: Option<bool>,
352    pub check_js: Option<bool>,
353    pub composite: Option<bool>,
354    pub declaration: Option<bool>,
355    pub declaration_map: Option<bool>,
356    pub downlevel_iteration: Option<bool>,
357    pub import_helpers: Option<bool>,
358    pub incremental: Option<bool>,
359    pub isolated_modules: Option<bool>,
360    pub jsx: Option<Jsx>,
361    pub lib: Option<Vec<Lib>>,
362    pub module: Option<Module>,
363    pub module_detection: Option<ModuleDetectionMode>,
364    pub no_emit: Option<bool>,
365    pub out_dir: Option<String>,
366    pub out_file: Option<String>,
367    pub remove_comments: Option<bool>,
368    pub root_dir: Option<String>,
369    pub source_map: Option<bool>,
370    pub target: Option<Target>,
371    pub ts_build_info_file: Option<String>,
372    pub always_strict: Option<bool>,
373    pub no_implicit_any: Option<bool>,
374    pub no_implicit_this: Option<bool>,
375    pub strict: Option<bool>,
376    pub strict_bind_call_apply: Option<bool>,
377    pub strict_function_types: Option<bool>,
378    pub strict_null_checks: Option<bool>,
379    pub strict_property_initialization: Option<bool>,
380    pub allow_synthetic_default_imports: Option<bool>,
381    pub allow_umd_global_access: Option<bool>,
382    pub base_url: Option<String>,
383    pub es_module_interop: Option<bool>,
384    pub module_resolution: Option<ModuleResolutionMode>,
385    pub paths: Option<HashMap<String, Vec<String>>>,
386    pub preserve_symlinks: Option<bool>,
387    pub root_dirs: Option<Vec<String>>,
388    pub type_roots: Option<Vec<String>>,
389    pub types: Option<Vec<String>>,
390    pub inline_source_map: Option<bool>,
391    pub inline_sources: Option<bool>,
392    pub map_root: Option<String>,
393    pub source_root: Option<String>,
394    pub no_fallthrough_cases_in_switch: Option<bool>,
395    pub no_implicit_returns: Option<bool>,
396    pub no_property_access_from_index_signature: Option<bool>,
397    pub no_unchecked_indexed_access: Option<bool>,
398    pub no_unused_locals: Option<bool>,
399    pub emit_decorator_metadata: Option<bool>,
400    pub experimental_decorators: Option<bool>,
401    pub allow_unreachable_code: Option<bool>,
402    pub allow_unused_labels: Option<bool>,
403    pub assume_changes_only_affect_direct_dependencies: Option<bool>,
404    #[deprecated]
405    pub charset: Option<String>,
406    pub declaration_dir: Option<String>,
407    #[deprecated]
408    pub diagnostics: Option<bool>,
409    pub disable_referenced_project_load: Option<bool>,
410    pub disable_size_limit: Option<bool>,
411    pub disable_solution_searching: Option<bool>,
412    pub disable_source_of_project_reference_redirect: Option<bool>,
413    #[serde(rename = "emitBOM")]
414    pub emit_bom: Option<bool>,
415    pub emit_declaration_only: Option<bool>,
416    pub explain_files: Option<bool>,
417    pub extended_diagnostics: Option<bool>,
418    pub force_consistent_casing_in_file_names: Option<bool>,
419    // XXX: Is generateCpuProfile available from tsconfig? Or just the CLI?
420    pub generate_cpu_profile: Option<bool>,
421
422    pub imports_not_used_as_values: Option<String>,
423    pub jsx_factory: Option<String>,
424    pub jsx_fragment_factory: Option<String>,
425    pub jsx_import_source: Option<String>,
426
427    pub keyof_strings_only: Option<bool>,
428    pub list_emitted_files: Option<bool>,
429    pub list_files: Option<bool>,
430    pub max_node_module_js_depth: Option<u32>,
431    pub no_emit_helpers: Option<bool>,
432    pub no_emit_on_error: Option<bool>,
433    pub no_error_truncation: Option<bool>,
434    pub no_implicit_use_strict: Option<bool>,
435    pub no_lib: Option<bool>,
436    pub no_resolve: Option<bool>,
437    pub no_strict_generic_checks: Option<bool>,
438    #[deprecated]
439    pub out: Option<String>,
440    pub preserve_const_enums: Option<bool>,
441    pub react_namespace: Option<String>,
442    pub resolve_json_module: Option<bool>,
443    pub skip_default_lib_check: Option<bool>,
444    pub skip_lib_check: Option<bool>,
445    pub strip_internal: Option<bool>,
446    pub suppress_excess_property_errors: Option<bool>,
447    pub suppress_implicit_any_index_errors: Option<bool>,
448    pub trace_resolution: Option<bool>,
449    pub use_define_for_class_fields: Option<bool>,
450    pub preserve_watch_output: Option<bool>,
451    pub pretty: Option<bool>,
452    pub fallback_polling: Option<String>,
453    pub watch_directory: Option<String>,
454    pub watch_file: Option<String>,
455}
456
457/// Module detection mode
458///
459/// This setting controls how TypeScript determines whether a file is a script or a module.
460/// These choices include:
461///   - "auto" (default) - TypeScript will not only look for import and export statements, but it will also check whether the "type" field in a package.json is set to "module" when running with module: nodenext or node16, and check whether the current file is a JSX file when running under jsx: react-jsx.
462///   - "legacy" - The same behavior as 4.6 and prior, usings import and export statements to determine whether a file is a module.
463///   - "force" - Ensures that every non-declaration file is treated as a module.
464#[derive(Deserialize, Debug, PartialEq, Copy, Clone, Default)]
465#[serde(rename_all = "lowercase")]
466pub enum ModuleDetectionMode {
467	#[default]
468	Auto,
469	Legacy,
470	Force,
471}
472
473/// Module resolution mode
474///
475/// Specify the module resolution strategy: 'node' (Node.js) or 'classic' (used in TypeScript before the release of 1.6). You probably won’t need to use classic in modern code.
476/// There is a handbook reference page [on Module Resolution](https://www.typescriptlang.org/docs/handbook/module-resolution.html).
477#[derive(Deserialize, Debug, PartialEq, Copy, Clone)]
478pub enum ModuleResolutionMode {
479    #[serde(rename = "classic", alias = "Classic")]
480    Classic,
481    #[serde(rename = "node", alias = "Node", alias = "node10", alias = "Node10")]
482    Node,
483    #[serde(rename = "node16", alias = "Node16")]
484    Node16,
485    #[serde(rename = "nodenext", alias = "NodeNext")]
486    NodeNext,
487    #[serde(rename = "bundler", alias = "Bundler")]
488    Bundler,
489}
490
491/// Controls how JSX constructs are emitted in JavaScript files. This only affects output of JS files that started in .tsx files.
492///
493///
494/// For example, this sample code:
495///
496/// ```tsx
497/// export const helloWorld = () => <h1>Hello world</h1>;
498/// ```
499///
500/// Default: "react"
501///
502/// ```tsx
503/// export const helloWorld = () => React.createElement("h1", null, "Hello world");
504/// ```
505///
506/// Preserve: "preserve"
507///
508/// ```tsx
509/// export const helloWorld = () => <h1>Hello world</h1>;
510/// ```
511///
512/// React Native: "react-native"
513///
514/// ```tsx
515/// export const helloWorld = () => <h1>Hello world</h1>;
516/// ```
517///
518/// React 17 transform: "react-jsx"
519///
520/// ```tsx
521/// import { jsx as _jsx } from "react/jsx-runtime";
522/// export const helloWorld = () => _jsx("h1", { children: "Hello world" }, void 0);
523/// ```
524///
525/// React 17 dev transform: "react-jsxdev"
526///
527/// ```tsx
528/// import { jsxDEV as _jsxDEV } from "react/jsx-dev-runtime";
529/// const _jsxFileName = "/home/runner/work/TypeScript-Website/TypeScript-Website/packages/typescriptlang-org/index.tsx";
530/// export const helloWorld = () => _jsxDEV("h1", { children: "Hello world" }, void 0, false, { fileName: _jsxFileName, lineNumber: 7, columnNumber: 32 }, this);
531/// ```
532#[derive(Deserialize, Debug, PartialEq, Copy, Clone)]
533#[serde(rename_all = "kebab-case")]
534pub enum Jsx {
535    /// Emit .js files with JSX changed to the equivalent React.createElement calls
536    React,
537    /// Emit .js files with the JSX changed to _jsx calls
538    ReactJsx,
539    /// Emit .js files with the JSX to _jsx calls
540    ReactJsxdev,
541    /// Emit .js files with the JSX unchanged
542    ReactNative,
543    /// Emit .jsx files with the JSX unchanged
544    Preserve,
545}
546
547/// The transpilation target for the emitted JavaScript.
548///
549/// Modern browsers support all `ES6` features, so `ES6` is a good choice. You might choose to set a lower target if your code
550/// is deployed to older environments, or a higher target if your code is guaranteed to run in newer environments.
551///
552/// The target setting changes which JS features are downleveled and which are left intact. For example, an arrow
553/// function () => this will be turned into an equivalent function expression if target is `ES5` or lower.
554///
555/// Changing target also changes the default value of lib. You may “mix and match” target and lib settings
556/// as desired, but you could just set target for convenience.
557///
558/// For developer platforms like Node will have a certain baselines for the their target depending on their version.
559/// You can find a set of community organized TSConfigs at tsconfig/bases for common platforms and their versions.
560///
561/// The special `ESNext` value refers to the highest version your version of TypeScript supports. This setting should be
562/// used with caution, since it doesn’t mean the same thing between different TypeScript versions and can
563/// make upgrades less predictable.
564#[derive(Debug, PartialEq, Clone)]
565pub enum Target {
566    Es3,
567    Es5,
568    Es2015,
569    Es6,
570    Es2016,
571    Es7,
572    Es2017,
573    Es2018,
574    Es2019,
575    Es2020,
576    EsNext,
577    Other(String),
578}
579impl<'de> Deserialize<'de> for Target {
580    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
581    where
582        D: Deserializer<'de>,
583    {
584        let s = String::deserialize(deserializer)?;
585        let s = s.to_uppercase();
586
587        let d = match s.as_str() {
588            "ES5" => Target::Es5,
589            "ES2015" => Target::Es2015,
590            "ES6" => Target::Es6,
591            "ES2016" => Target::Es2016,
592            "ES7" => Target::Es7,
593            "ES2017" => Target::Es2017,
594            "ES2018" => Target::Es2018,
595            "ES2019" => Target::Es2019,
596            "ES2020" => Target::Es2020,
597            "ESNEXT" => Target::EsNext,
598            other => Target::Other(other.to_string()),
599        };
600
601        Ok(d)
602    }
603}
604
605/// Available definitions for built-in JS APIs.
606///
607/// TypeScript includes a default set of type definitions for built-in JS APIs (like Math), as well as type definitions for things found in browser environments (like document). TypeScript also includes APIs for newer JS features matching the target you specify; for example the definition for Map is available if target is ES6 or newer.
608///
609/// You may want to change these for a few reasons:
610///
611/// * Your program doesn't run in a browser, so you don’t want the "dom" type definitions
612/// * Your runtime platform provides certain JavaScript API objects (maybe through polyfills), but doesn't yet support the full syntax of a given ECMAScript version
613/// * You have polyfills or native implementations for some, but not all, of a higher level ECMAScript version
614///
615#[derive(Debug, PartialEq, Clone)]
616pub enum Lib {
617    Es5,
618    Es2015,
619    Es6,
620    Es2016,
621    Es7,
622    Es2017,
623    Es2018,
624    Es2019,
625    Es2020,
626    EsNext,
627    Dom,
628    WebWorker,
629    ScriptHost,
630    DomIterable,
631    Es2015Core,
632    Es2015Generator,
633    Es2015Iterable,
634    Es2015Promise,
635    Es2015Proxy,
636    Es2015Reflect,
637    Es2015Symbol,
638    Es2015SymbolWellKnown,
639    Es2016ArrayInclude,
640    Es2017Object,
641    Es2017Intl,
642    Es2017SharedMemory,
643    Es2017String,
644    Es2017TypedArrays,
645    Es2018Intl,
646    Es2018Promise,
647    Es2018RegExp,
648    Es2019Array,
649    Es2019Object,
650    Es2019String,
651    Es2019Symbol,
652    Es2020String,
653    Es2020SymbolWellknown,
654    EsNextAsyncIterable,
655    EsNextArray,
656    EsNextIntl,
657    EsNextSymbol,
658    Other(String),
659}
660
661impl<'de> Deserialize<'de> for Lib {
662    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
663    where
664        D: Deserializer<'de>,
665    {
666        let s = String::deserialize(deserializer)?;
667        let s = s.to_uppercase();
668
669        let d = match s.as_str() {
670            "ES5" => Lib::Es5,
671            "ES2015" => Lib::Es2015,
672            "ES6" => Lib::Es6,
673            "ES2016" => Lib::Es2016,
674            "ES7" => Lib::Es7,
675            "ES2017" => Lib::Es2017,
676            "ES2018" => Lib::Es2018,
677            "ES2019" => Lib::Es2019,
678            "ES2020" => Lib::Es2020,
679            "ESNext" => Lib::EsNext,
680            "DOM" => Lib::Dom,
681            "WEBWORKER" => Lib::WebWorker,
682            "SCRIPTHOST" => Lib::ScriptHost,
683            "DOM.ITERABLE" => Lib::DomIterable,
684            "ES2015.CORE" => Lib::Es2015Core,
685            "ES2015.GENERATOR" => Lib::Es2015Generator,
686            "ES2015.ITERABLE" => Lib::Es2015Iterable,
687            "ES2015.PROMISE" => Lib::Es2015Promise,
688            "ES2015.PROXY" => Lib::Es2015Proxy,
689            "ES2015.REFLECT" => Lib::Es2015Reflect,
690            "ES2015.SYMBOL" => Lib::Es2015Symbol,
691            "ES2015.SYMBOL.WELLKNOWN" => Lib::Es2015SymbolWellKnown,
692            "ES2015.ARRAY.INCLUDE" => Lib::Es2016ArrayInclude,
693            "ES2015.OBJECT" => Lib::Es2017Object,
694            "ES2017INTL" => Lib::Es2017Intl,
695            "ES2015.SHAREDMEMORY" => Lib::Es2017SharedMemory,
696            "ES2017.STRING" => Lib::Es2017String,
697            "ES2017.TYPEDARRAYS" => Lib::Es2017TypedArrays,
698            "ES2018.INTL" => Lib::Es2018Intl,
699            "ES2018.PROMISE" => Lib::Es2018Promise,
700            "ES2018.REGEXP" => Lib::Es2018RegExp,
701            "ES2019.ARRAY" => Lib::Es2019Array,
702            "ES2019.OBJECT" => Lib::Es2019Object,
703            "ES2019.STRING" => Lib::Es2019String,
704            "ES2019.SYMBOL" => Lib::Es2019Symbol,
705            "ES2020.STRING" => Lib::Es2020String,
706            "ES2020.SYMBOL.WELLKNOWN" => Lib::Es2020SymbolWellknown,
707            "ESNEXT.ASYNCITERABLE" => Lib::EsNextAsyncIterable,
708            "ESNEXT.ARRAY" => Lib::EsNextArray,
709            "ESNEXT.INTL" => Lib::EsNextIntl,
710            "ESNEXT.SYMBOL" => Lib::EsNextSymbol,
711            other => Lib::Other(other.to_string()),
712        };
713
714        Ok(d)
715    }
716}
717
718/// Sets the module system for the program.
719///
720/// See the [Modules reference page](https://www.typescriptlang.org/docs/handbook/modules.html)
721/// for more information. You very likely want "CommonJS" for node projects.
722///
723/// Changing module affects moduleResolution which also has a reference page.
724///
725/// Here’s some example output for this file:
726///
727/// ```tsx
728/// // @filename: index.ts
729/// import { valueOfPi } from "./constants";
730///
731///
732/// export const twoPi = valueOfPi * 2;
733/// ```
734///
735/// ## CommonJS
736///
737/// ```js
738/// "use strict";
739/// Object.defineProperty(exports, "__esModule", { value: true });
740/// exports.twoPi = void 0;
741/// const constants_1 = require("./constants");
742/// exports.twoPi = constants_1.valueOfPi * 2;
743/// ```
744///
745/// ## UMD
746///
747/// ```js
748/// (function (factory) {
749///     if (typeof module === "object" && typeof module.exports === "object") {
750///         var v = factory(require, exports);
751///         if (v !== undefined) module.exports = v;
752///     }
753///     else if (typeof define === "function" && define.amd) {
754///         define(["require", "exports", "./constants"], factory);
755///     }
756/// })(function (require, exports) {
757///     "use strict";
758///     Object.defineProperty(exports, "__esModule", { value: true });
759///     exports.twoPi = void 0;
760///     const constants_1 = require("./constants");
761///     exports.twoPi = constants_1.valueOfPi * 2;
762/// });
763/// ```
764///
765/// ## AMD
766///
767/// ```js
768/// define(["require", "exports", "./constants"], function (require, exports, constants_1) {
769///     "use strict";
770///     Object.defineProperty(exports, "__esModule", { value: true });
771///     exports.twoPi = void 0;
772///     exports.twoPi = constants_1.valueOfPi * 2;
773/// });
774/// ```
775///
776/// ## System
777///
778/// ```js
779/// System.register(["./constants"], function (exports_1, context_1) {
780///     "use strict";
781///     var constants_1, twoPi;
782///     var __moduleName = context_1 && context_1.id;
783///     return {
784///         setters: [
785///             function (constants_1_1) {
786///                 constants_1 = constants_1_1;
787///             }
788///         ],
789///         execute: function () {
790///             exports_1("twoPi", twoPi = constants_1.valueOfPi * 2);
791///         }
792///     };
793/// });
794/// ```
795///
796/// ## ESNext
797///
798/// ```js
799/// import { valueOfPi } from "./constants";
800/// export const twoPi = valueOfPi * 2;
801/// ```
802///
803/// ## ES2020
804///
805/// ```js
806/// import { valueOfPi } from "./constants";
807/// export const twoPi = valueOfPi * 2;
808/// ```
809///
810/// ## None
811///
812/// ```js
813/// "use strict";
814/// Object.defineProperty(exports, "__esModule", { value: true });
815/// exports.twoPi = void 0;
816/// const constants_1 = require("./constants");
817/// exports.twoPi = constants_1.valueOfPi * 2;
818/// ```
819#[derive(Debug, Clone, PartialEq)]
820pub enum Module {
821    CommonJs,
822    Es6,
823    Es2015,
824    Es2020,
825    None,
826    Umd,
827    Amd,
828    System,
829    EsNext,
830    Other(String),
831}
832
833impl<'de> Deserialize<'de> for Module {
834    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
835    where
836        D: Deserializer<'de>,
837    {
838        let s = String::deserialize(deserializer)?;
839        let s = s.to_uppercase();
840
841        let r = match s.as_str() {
842            "COMMONJS" => Module::CommonJs,
843            "ESNEXT" => Module::EsNext,
844            "ES6" => Module::Es6,
845            "ES2015" => Module::Es2015,
846            "ES2020" => Module::Es2020,
847            "NONE" => Module::None,
848            "UMD" => Module::Umd,
849            "AMD" => Module::Amd,
850            "SYSTEM" => Module::System,
851            other => Module::Other(other.to_string()),
852        };
853
854        Ok(r)
855    }
856}
857
858#[cfg(test)]
859mod test {
860    use super::*;
861    #[test]
862    fn parse_jsx() {
863        let json = r#"{"compilerOptions": {"jsx": "react-jsx"}}"#;
864
865        let config = TsConfig::parse_str(json).unwrap();
866        assert_eq!(config.compiler_options.unwrap().jsx, Some(Jsx::ReactJsx));
867    }
868
869    #[test]
870    fn parse_paths() {
871        let json = r#"{
872        "compilerOptions": {
873            "baseUrl": "src",
874            "paths": {
875                "tests/*": ["tests/*"],
876                "blah": ["bloop"]
877            }
878        }
879    }
880        
881        "#;
882
883        let config = TsConfig::parse_str(json).unwrap();
884        assert_eq!(
885            config
886                .compiler_options
887                .unwrap()
888                .paths
889                .unwrap()
890                .get("tests/*"),
891            Some(&vec!["tests/*".to_string()])
892        );
893    }
894
895    #[test]
896    fn parse_empty() {
897        TsConfig::parse_str("{}").unwrap();
898        TsConfig::parse_str(r#"{"compilerOptions": {}}"#).unwrap();
899    }
900
901    #[test]
902    fn parse_default() {
903        let json = include_str!("../test/tsconfig.default.json");
904        TsConfig::parse_str(json).unwrap();
905    }
906
907    #[test]
908    fn parse_common_tsconfig() {
909        let json = include_str!("../test/tsconfig.common.json");
910        TsConfig::parse_str(json).unwrap();
911    }
912
913    #[test]
914    fn parse_complete_tsconfig() {
915        let json = include_str!("../test/tsconfig.complete.json");
916        TsConfig::parse_str(json).unwrap();
917    }
918
919    #[test]
920    fn ignores_invalid_fields() {
921        let json = r#"{"bleep": true, "compilerOptions": {"someNewUnsupportedProperty": false}}"#;
922        TsConfig::parse_str(json).unwrap();
923    }
924
925    #[test]
926    fn ignores_dangling_commas() {
927        let json = r#"{"compilerOptions": {"noImplicitAny": false,"explainFiles": true,}}"#;
928        let cfg = TsConfig::parse_str(json).unwrap();
929        assert_eq!(cfg.compiler_options.unwrap().explain_files.unwrap(), true);
930
931        let json = r#"{"compilerOptions": {"noImplicitAny": false,"explainFiles": true, }}"#;
932        let cfg = TsConfig::parse_str(json).unwrap();
933        assert_eq!(cfg.compiler_options.unwrap().explain_files.unwrap(), true);
934
935        let json = r#"{"compilerOptions": {"noImplicitAny": false,"explainFiles": true,
936    }}"#;
937        let cfg = TsConfig::parse_str(json).unwrap();
938        assert_eq!(cfg.compiler_options.unwrap().explain_files.unwrap(), true);
939    }
940
941    #[test]
942    fn merge_two_configs() {
943        let json_1 = r#"{"compilerOptions": {"jsx": "react", "noEmit": true,}}"#;
944        let json_2 = r#"{"compilerOptions": {"jsx": "preserve", "removeComments": true}}"#;
945
946        let mut value1: Value = parse_to_value(json_1).unwrap();
947        let value2: Value = parse_to_value(json_2).unwrap();
948
949        merge(&mut value1, value2);
950
951        let value: TsConfig = serde_json::from_value(value1).unwrap();
952
953        assert_eq!(
954            value.clone().compiler_options.unwrap().jsx,
955            Some(Jsx::React)
956        );
957        assert_eq!(value.clone().compiler_options.unwrap().no_emit, Some(true));
958        assert_eq!(value.compiler_options.unwrap().remove_comments, Some(true));
959    }
960
961    #[test]
962    fn parse_basic_file() {
963        let path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
964            .join("test/tsconfig.default.json");
965        let config = TsConfig::parse_file(&path).unwrap();
966
967        assert_eq!(
968            config.compiler_options.clone().unwrap().target,
969            Some(Target::Es5)
970        );
971        assert_eq!(
972            config.compiler_options.clone().unwrap().module,
973            Some(Module::CommonJs)
974        );
975        assert_eq!(config.compiler_options.unwrap().strict, Some(true));
976    }
977
978    #[test]
979    fn parse_inheriting_file() {
980        let path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
981            .join("test/tsconfig.inherits.json");
982        let config = TsConfig::parse_file(&path).unwrap();
983
984        assert_eq!(
985            config
986                .compiler_options
987                .clone()
988                .unwrap()
989                .use_define_for_class_fields,
990            Some(false)
991        );
992
993        assert_eq!(
994            config.compiler_options.clone().unwrap().declaration,
995            Some(true)
996        );
997
998        assert_eq!(
999            config.compiler_options.unwrap().trace_resolution,
1000            Some(false)
1001        );
1002    }
1003
1004    #[test]
1005    fn parse_inheritance_chain() {
1006        let path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
1007            .join("test/a/tsconfig.inherits_again.json");
1008        let config = TsConfig::parse_file(&path).unwrap();
1009
1010        assert_eq!(
1011            config
1012                .compiler_options
1013                .clone()
1014                .unwrap()
1015                .use_define_for_class_fields,
1016            Some(false)
1017        );
1018
1019        assert_eq!(
1020            config.compiler_options.clone().unwrap().declaration,
1021            Some(true)
1022        );
1023
1024        assert_eq!(
1025            config.compiler_options.clone().unwrap().trace_resolution,
1026            Some(false)
1027        );
1028
1029        assert_eq!(config.compiler_options.unwrap().jsx, Some(Jsx::ReactNative));
1030    }
1031
1032    #[test]
1033    fn parse_no_extension_file() {
1034        let path = Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap())
1035            .join("test/tsconfig.noextension.json");
1036        let config = TsConfig::parse_file(&path).unwrap();
1037
1038        assert_eq!(config.compiler_options.clone().unwrap().strict, Some(true));
1039
1040        assert_eq!(
1041            config.compiler_options.clone().unwrap().declaration,
1042            Some(true)
1043        );
1044
1045        assert_eq!(
1046            config.compiler_options.unwrap().trace_resolution,
1047            Some(false)
1048        );
1049    }
1050}