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}