diff --git a/Cargo.lock b/Cargo.lock index 4253fc51d5003c..1dce0138f6184b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,6 +16,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" +[[package]] +name = "ahash" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" +dependencies = [ + "const-random", +] + [[package]] name = "aho-corasick" version = "0.7.13" @@ -265,6 +274,26 @@ dependencies = [ "bitflags", ] +[[package]] +name = "const-random" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a" +dependencies = [ + "const-random-macro", + "proc-macro-hack", +] + +[[package]] +name = "const-random-macro" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a" +dependencies = [ + "getrandom", + "proc-macro-hack", +] + [[package]] name = "crc32fast" version = "1.2.0" @@ -330,6 +359,17 @@ dependencies = [ "syn 1.0.36", ] +[[package]] +name = "dashmap" +version = "3.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f260e2fc850179ef410018660006951c1b55b79e8087e87111a2c388994b9b5" +dependencies = [ + "ahash", + "cfg-if", + "num_cpus", +] + [[package]] name = "deno" version = "1.3.2" @@ -2262,6 +2302,7 @@ checksum = "c3e99fc5c5b87871fa19036fd8a622ecf6b2a29b30b4d632e48f6a1923e393ea" dependencies = [ "Inflector", "arrayvec", + "dashmap", "either", "fxhash", "indexmap", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3ecb5ca2349cc6..a8d1e6a997887c 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -62,7 +62,7 @@ serde_json = { version = "1.0.57", features = [ "preserve_order" ] } sys-info = "0.7.0" sourcemap = "6.0.1" swc_common = { version = "=0.10.2", features = ["sourcemap"] } -swc_ecmascript = { version = "=0.6.3", features = ["codegen", "parser", "transforms", "visit"] } +swc_ecmascript = { version = "=0.6.3", features = ["codegen", "parser", "react", "transforms", "visit"] } tempfile = "3.1.0" termcolor = "1.1.0" tokio = { version = "0.2.22", features = ["full"] } diff --git a/cli/swc_util.rs b/cli/swc_util.rs index 7c7d0183237e30..1a058d6da896c5 100644 --- a/cli/swc_util.rs +++ b/cli/swc_util.rs @@ -29,6 +29,10 @@ use swc_ecmascript::parser::StringInput; use swc_ecmascript::parser::Syntax; use swc_ecmascript::parser::TsConfig; use swc_ecmascript::transforms::fixer; +use swc_ecmascript::transforms::helpers; +use swc_ecmascript::transforms::pass::Optional; +use swc_ecmascript::transforms::proposals::decorators; +use swc_ecmascript::transforms::react; use swc_ecmascript::transforms::typescript; use swc_ecmascript::visit::FoldWith; @@ -230,52 +234,6 @@ impl AstParser { }) } - pub fn strip_types( - &self, - file_name: &str, - media_type: MediaType, - source_code: &str, - ) -> Result { - let module = self.parse_module(file_name, media_type, source_code)?; - let program = Program::Module(module); - let mut compiler_pass = - chain!(typescript::strip(), fixer(Some(&self.comments))); - let program = program.fold_with(&mut compiler_pass); - - let mut src_map_buf = vec![]; - let mut buf = vec![]; - { - let writer = Box::new(JsWriter::new( - self.source_map.clone(), - "\n", - &mut buf, - Some(&mut src_map_buf), - )); - let config = swc_ecmascript::codegen::Config { minify: false }; - let mut emitter = swc_ecmascript::codegen::Emitter { - cfg: config, - comments: Some(&self.comments), - cm: self.source_map.clone(), - wr: writer, - }; - program.emit_with(&mut emitter)?; - } - let mut src = String::from_utf8(buf)?; - { - let mut buf = vec![]; - self - .source_map - .build_source_map_from(&mut src_map_buf, None) - .to_writer(&mut buf)?; - let map = String::from_utf8(buf)?; - - src.push_str("//# sourceMappingURL=data:application/json;base64,"); - let encoded_map = base64::encode(map.as_bytes()); - src.push_str(&encoded_map); - } - Ok(src) - } - pub fn get_span_location(&self, span: Span) -> swc_common::Loc { self.source_map.lookup_char_pos(span.lo()) } @@ -290,13 +248,196 @@ impl AstParser { } } -#[test] -fn test_strip_types() { +#[derive(Debug, Clone)] +pub struct EmitTranspileOptions { + /// When emitting a legacy decorator, also emit experimental decorator meta + /// data. Defaults to `false`. + pub emit_metadata: bool, + /// Should the source map be inlined in the emitted code file, or provided + /// as a separate file. Defaults to `true`. + pub inline_source_map: bool, + /// When transforming JSX, what value should be used for the JSX factory. + /// Defaults to `React.createElement`. + pub jsx_factory: String, + /// When transforming JSX, what value should be used for the JSX fragment + /// factory. Defaults to `React.Fragment`. + pub jsx_fragment_factory: String, + /// Should JSX be transformed or preserved. Defaults to `true`. + pub transform_jsx: bool, +} + +impl Default for EmitTranspileOptions { + fn default() -> Self { + EmitTranspileOptions { + emit_metadata: false, + inline_source_map: true, + jsx_factory: "React.createElement".into(), + jsx_fragment_factory: "React.Fragment".into(), + transform_jsx: true, + } + } +} + +pub fn transpile( + file_name: &str, + media_type: MediaType, + source_code: &str, + options: &EmitTranspileOptions, +) -> Result<(String, Option), ErrBox> { let ast_parser = AstParser::default(); - let result = ast_parser - .strip_types("test.ts", MediaType::TypeScript, "const a: number = 10;") + let module = ast_parser.parse_module(file_name, media_type, source_code)?; + let program = Program::Module(module); + + let jsx_pass = react::react( + ast_parser.source_map.clone(), + react::Options { + pragma: options.jsx_factory.clone(), + pragma_frag: options.jsx_fragment_factory.clone(), + // this will use `Object.assign()` instead of the `_extends` helper + // when spreading props. + use_builtins: true, + ..Default::default() + }, + ); + let mut passes = chain!( + Optional::new(jsx_pass, options.transform_jsx), + decorators::decorators(decorators::Config { + legacy: true, + emit_metadata: options.emit_metadata, + }), + typescript::strip(), + fixer(Some(&ast_parser.comments)), + ); + + let program = swc_common::GLOBALS.set(&Globals::new(), || { + helpers::HELPERS.set(&helpers::Helpers::new(false), || { + program.fold_with(&mut passes) + }) + }); + + let mut src_map_buf = vec![]; + let mut buf = vec![]; + { + let writer = Box::new(JsWriter::new( + ast_parser.source_map.clone(), + "\n", + &mut buf, + Some(&mut src_map_buf), + )); + let config = swc_ecmascript::codegen::Config { minify: false }; + let mut emitter = swc_ecmascript::codegen::Emitter { + cfg: config, + comments: Some(&ast_parser.comments), + cm: ast_parser.source_map.clone(), + wr: writer, + }; + program.emit_with(&mut emitter)?; + } + let mut src = String::from_utf8(buf)?; + let mut map: Option = None; + { + let mut buf = Vec::new(); + ast_parser + .source_map + .build_source_map_from(&mut src_map_buf, None) + .to_writer(&mut buf)?; + + if options.inline_source_map { + src.push_str("//# sourceMappingURL=data:application/json;base64,"); + let encoded_map = base64::encode(buf); + src.push_str(&encoded_map); + } else { + map = Some(String::from_utf8(buf)?); + } + } + Ok((src, map)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_transpile() { + let source = r#" + enum D { + A, + B, + C, + } + export class A { + private b: string; + protected c: number = 1; + e: "foo"; + constructor (public d = D.A) { + const e = "foo" as const; + this.e = e; + } + } + "#; + let result = transpile( + "test.ts", + MediaType::TypeScript, + source, + &EmitTranspileOptions::default(), + ) .unwrap(); - assert!(result.starts_with( - "const a = 10;\n//# sourceMappingURL=data:application/json;base64," - )); + let (code, maybe_map) = result; + assert!(code.starts_with("var D;\n(function(D) {\n")); + assert!( + code.contains("\n//# sourceMappingURL=data:application/json;base64,") + ); + assert!(maybe_map.is_none()); + } + + #[test] + fn test_transpile_tsx() { + let source = r#" + export class A { + render() { + return
+ } + } + "#; + let result = transpile( + "test.ts", + MediaType::TSX, + source, + &EmitTranspileOptions::default(), + ) + .unwrap(); + let (code, _maybe_source_map) = result; + assert!(code.contains("React.createElement(\"div\", null")); + } + + #[test] + fn test_transpile_decorators() { + let source = r#" + function enumerable(value: boolean) { + return function ( + _target: any, + _propertyKey: string, + descriptor: PropertyDescriptor, + ) { + descriptor.enumerable = value; + }; + } + + export class A { + @enumerable(false) + a() { + Test.value; + } + } + "#; + let result = transpile( + "test.ts", + MediaType::TypeScript, + source, + &EmitTranspileOptions::default(), + ) + .unwrap(); + let (code, _maybe_source_map) = result; + assert!(code.contains("_applyDecoratedDescriptor(")); + } } diff --git a/cli/tsc.rs b/cli/tsc.rs index cc902d196db4fb..79967369497ad5 100644 --- a/cli/tsc.rs +++ b/cli/tsc.rs @@ -18,6 +18,7 @@ use crate::permissions::Permissions; use crate::source_maps::SourceMapGetter; use crate::startup_data; use crate::state::State; +use crate::swc_util; use crate::swc_util::AstParser; use crate::swc_util::Location; use crate::swc_util::SwcDiagnosticBuffer; @@ -418,6 +419,16 @@ struct CompileResponse { stats: Option>, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TranspileTsOptions { + check_js: bool, + emit_decorator_metadata: bool, + jsx: String, + jsx_factory: String, + jsx_fragment_factory: String, +} + // TODO(bartlomieju): possible deduplicate once TS refactor is stabilized #[derive(Deserialize)] #[serde(rename_all = "camelCase")] @@ -795,12 +806,39 @@ impl TsCompiler { let mut emit_map = HashMap::new(); + let mut compiler_options = json!({ + "checkJs": false, + "emitDecoratorMetadata": false, + "jsx": "react", + "jsxFactory": "React.createElement", + "jsxFragmentFactory": "React.Fragment", + }); + + let compiler_config = self.config.clone(); + + 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 compiler_options: TranspileTsOptions = + serde_json::from_value(compiler_options)?; + + let transpile_options = swc_util::EmitTranspileOptions { + emit_metadata: compiler_options.emit_decorator_metadata, + inline_source_map: true, + jsx_factory: compiler_options.jsx_factory, + jsx_fragment_factory: compiler_options.jsx_fragment_factory, + transform_jsx: compiler_options.jsx == "react", + }; for source_file in source_files { - let parser = AstParser::default(); - let stripped_source = parser.strip_types( + let (stripped_source, _maybe_source_map) = swc_util::transpile( &source_file.file_name, MediaType::TypeScript, &source_file.source_code, + &transpile_options, )?; // TODO(bartlomieju): this is superfluous, just to make caching function happy