// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use crate::msg::MediaType; use deno_core::ErrBox; use serde::Serialize; use std::error::Error; use std::fmt; use std::rc::Rc; use std::sync::Arc; use std::sync::RwLock; use swc_common::chain; use swc_common::comments::SingleThreadedComments; use swc_common::errors::Diagnostic; use swc_common::errors::DiagnosticBuilder; use swc_common::errors::Emitter; use swc_common::errors::Handler; use swc_common::errors::HandlerFlags; use swc_common::FileName; use swc_common::Globals; use swc_common::SourceMap; use swc_common::Span; use swc_ecmascript::ast::Program; use swc_ecmascript::codegen::text_writer::JsWriter; use swc_ecmascript::codegen::Node; use swc_ecmascript::parser::lexer::Lexer; use swc_ecmascript::parser::EsConfig; use swc_ecmascript::parser::JscTarget; use swc_ecmascript::parser::Parser; 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; #[derive(Debug, Serialize, Clone, PartialEq)] pub struct Location { pub filename: String, pub line: usize, pub col: usize, } impl Into for swc_common::Loc { fn into(self) -> Location { use swc_common::FileName::*; let filename = match &self.file.name { Real(path_buf) => path_buf.to_string_lossy().to_string(), Custom(str_) => str_.to_string(), _ => panic!("invalid filename"), }; Location { filename, line: self.line, col: self.col_display, } } } fn get_default_es_config() -> EsConfig { let mut config = EsConfig::default(); config.num_sep = true; config.class_private_props = true; config.class_private_methods = true; config.class_props = true; config.export_default_from = true; config.export_namespace_from = true; config.dynamic_import = true; config.nullish_coalescing = true; config.optional_chaining = true; config.import_meta = true; config.top_level_await = true; config } fn get_default_ts_config() -> TsConfig { let mut ts_config = TsConfig::default(); ts_config.dynamic_import = true; ts_config.decorators = true; ts_config } pub fn get_syntax_for_dts() -> Syntax { let mut ts_config = TsConfig::default(); ts_config.dts = true; Syntax::Typescript(ts_config) } pub fn get_syntax_for_media_type(media_type: MediaType) -> Syntax { match media_type { MediaType::JavaScript => Syntax::Es(get_default_es_config()), MediaType::JSX => { let mut config = get_default_es_config(); config.jsx = true; Syntax::Es(config) } MediaType::TypeScript => Syntax::Typescript(get_default_ts_config()), MediaType::TSX => { let mut config = get_default_ts_config(); config.tsx = true; Syntax::Typescript(config) } _ => Syntax::Es(get_default_es_config()), } } #[derive(Clone, Debug)] pub struct SwcDiagnosticBuffer { pub diagnostics: Vec, } impl Error for SwcDiagnosticBuffer {} impl fmt::Display for SwcDiagnosticBuffer { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let msg = self.diagnostics.join(","); f.pad(&msg) } } impl SwcDiagnosticBuffer { pub fn from_swc_error( error_buffer: SwcErrorBuffer, parser: &AstParser, ) -> Self { let s = error_buffer.0.read().unwrap().clone(); let diagnostics = s .iter() .map(|d| { let mut msg = d.message(); if let Some(span) = d.span.primary_span() { let location = parser.get_span_location(span); let filename = match &location.file.name { FileName::Custom(n) => n, _ => unreachable!(), }; msg = format!( "{} at {}:{}:{}", msg, filename, location.line, location.col_display ); } msg }) .collect::>(); Self { diagnostics } } } #[derive(Clone)] pub struct SwcErrorBuffer(Arc>>); impl SwcErrorBuffer { pub fn default() -> Self { Self(Arc::new(RwLock::new(vec![]))) } } impl Emitter for SwcErrorBuffer { fn emit(&mut self, db: &DiagnosticBuilder) { self.0.write().unwrap().push((**db).clone()); } } /// Low-level utility structure with common AST parsing functions. /// /// Allows to build more complicated parser by providing a callback /// to `parse_module`. pub struct AstParser { pub buffered_error: SwcErrorBuffer, pub source_map: Rc, pub handler: Handler, pub comments: SingleThreadedComments, pub globals: Globals, } impl AstParser { pub fn default() -> Self { let buffered_error = SwcErrorBuffer::default(); let handler = Handler::with_emitter_and_flags( Box::new(buffered_error.clone()), HandlerFlags { dont_buffer_diagnostics: true, can_emit_warnings: true, ..Default::default() }, ); AstParser { buffered_error, source_map: Rc::new(SourceMap::default()), handler, comments: SingleThreadedComments::default(), globals: Globals::new(), } } pub fn parse_module( &self, file_name: &str, media_type: MediaType, source_code: &str, ) -> Result { let swc_source_file = self.source_map.new_source_file( FileName::Custom(file_name.to_string()), source_code.to_string(), ); let buffered_err = self.buffered_error.clone(); let syntax = get_syntax_for_media_type(media_type); let lexer = Lexer::new( syntax, JscTarget::Es2019, StringInput::from(&*swc_source_file), Some(&self.comments), ); let mut parser = Parser::new_from(lexer); parser.parse_module().map_err(move |err| { let mut diagnostic = err.into_diagnostic(&self.handler); diagnostic.emit(); SwcDiagnosticBuffer::from_swc_error(buffered_err, self) }) } pub fn get_span_location(&self, span: Span) -> swc_common::Loc { self.source_map.lookup_char_pos(span.lo()) } pub fn get_span_comments( &self, span: Span, ) -> Vec { self .comments .with_leading(span.lo(), |comments| comments.to_vec()) } } #[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 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(), Some(&ast_parser.comments), 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(); 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(")); } }