// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use crate::config_file; use crate::text_encoding::strip_bom; use deno_ast::get_syntax; use deno_ast::swc::ast::Module; use deno_ast::swc::ast::Program; use deno_ast::swc::codegen::text_writer::JsWriter; use deno_ast::swc::codegen::Node; use deno_ast::swc::common::chain; use deno_ast::swc::common::comments::SingleThreadedComments; use deno_ast::swc::common::errors::Diagnostic as SwcDiagnostic; use deno_ast::swc::common::BytePos; use deno_ast::swc::common::FileName; use deno_ast::swc::common::Globals; use deno_ast::swc::common::Mark; use deno_ast::swc::common::SourceMap; use deno_ast::swc::common::Spanned; use deno_ast::swc::parser::error::Error as SwcError; use deno_ast::swc::parser::error::SyntaxError; use deno_ast::swc::parser::lexer::Lexer; use deno_ast::swc::parser::StringInput; use deno_ast::swc::transforms::fixer; use deno_ast::swc::transforms::helpers; use deno_ast::swc::transforms::hygiene; use deno_ast::swc::transforms::pass::Optional; use deno_ast::swc::transforms::proposals; use deno_ast::swc::transforms::react; use deno_ast::swc::transforms::resolver_with_mark; use deno_ast::swc::transforms::typescript; use deno_ast::swc::visit::FoldWith; use deno_ast::Diagnostic; use deno_ast::LineAndColumnDisplay; use deno_ast::MediaType; use deno_ast::ParsedSource; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::resolve_url_or_path; use deno_core::serde_json; use deno_core::ModuleSpecifier; use std::cell::RefCell; use std::fmt; use std::rc::Rc; mod bundle_hook; mod transforms; pub use bundle_hook::BundleHook; #[derive(Debug, Clone, Eq, PartialEq)] pub struct Location { pub specifier: String, pub line: usize, pub col: usize, } impl Location { pub fn from_pos(parsed_source: &ParsedSource, pos: BytePos) -> Self { Location::from_line_and_column( parsed_source.specifier().to_string(), parsed_source.source().line_and_column_index(pos), ) } pub fn from_line_and_column( specifier: String, line_and_column: deno_ast::LineAndColumnIndex, ) -> Self { Location { specifier, line: line_and_column.line_index + 1, col: line_and_column.column_index, } } } impl From for Location { fn from(swc_loc: deno_ast::swc::common::Loc) -> Self { use deno_ast::swc::common::FileName::*; let filename = match &swc_loc.file.name { Real(path_buf) => path_buf.to_string_lossy().to_string(), Custom(str_) => str_.to_string(), Url(url) => url.to_string(), _ => panic!("invalid filename"), }; Location { specifier: filename, line: swc_loc.line, col: swc_loc.col.0, } } } impl From for ModuleSpecifier { fn from(loc: Location) -> Self { resolve_url_or_path(&loc.specifier).unwrap() } } impl std::fmt::Display for Location { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{}:{}:{}", self.specifier, self.line, self.col) } } #[derive(Debug)] pub struct Diagnostics(pub Vec); impl std::error::Error for Diagnostics {} impl fmt::Display for Diagnostics { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (i, diagnostic) in self.0.iter().enumerate() { if i > 0 { write!(f, "\n\n")?; } write!(f, "{}", diagnostic)? } Ok(()) } } #[derive(Debug, Clone)] pub enum ImportsNotUsedAsValues { Remove, Preserve, Error, } /// Options which can be adjusted when transpiling a module. #[derive(Debug, Clone)] pub struct EmitOptions { /// When emitting a legacy decorator, also emit experimental decorator meta /// data. Defaults to `false`. pub emit_metadata: bool, /// What to do with import statements that only import types i.e. whether to /// remove them (`Remove`), keep them as side-effect imports (`Preserve`) /// or error (`Error`). Defaults to `Remove`. pub imports_not_used_as_values: ImportsNotUsedAsValues, /// 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, /// Should the sources be inlined in the source map. Defaults to `true`. pub inline_sources: bool, /// Should a corresponding .map file be created for the output. This should be /// false if inline_source_map is true. Defaults to `false`. pub source_map: bool, /// `true` if the program should use an implicit JSX import source/the "new" /// JSX transforms. pub jsx_automatic: bool, /// If JSX is automatic, if it is in development mode, meaning that it should /// import `jsx-dev-runtime` and transform JSX using `jsxDEV` import from the /// JSX import source as well as provide additional debug information to the /// JSX factory. pub jsx_development: 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, /// The string module specifier to implicitly import JSX factories from when /// transpiling JSX. pub jsx_import_source: Option, /// Should JSX be transformed or preserved. Defaults to `true`. pub transform_jsx: bool, /// Should import declarations be transformed to variable declarations. /// This should only be set to true for the REPL. Defaults to `false`. pub repl_imports: bool, } impl Default for EmitOptions { fn default() -> Self { EmitOptions { emit_metadata: false, imports_not_used_as_values: ImportsNotUsedAsValues::Remove, inline_source_map: true, inline_sources: true, source_map: false, jsx_automatic: false, jsx_development: false, jsx_factory: "React.createElement".into(), jsx_fragment_factory: "React.Fragment".into(), jsx_import_source: None, transform_jsx: true, repl_imports: false, } } } impl From for EmitOptions { fn from(config: config_file::TsConfig) -> Self { let options: config_file::EmitConfigOptions = serde_json::from_value(config.0).unwrap(); let imports_not_used_as_values = match options.imports_not_used_as_values.as_str() { "preserve" => ImportsNotUsedAsValues::Preserve, "error" => ImportsNotUsedAsValues::Error, _ => ImportsNotUsedAsValues::Remove, }; let (transform_jsx, jsx_automatic, jsx_development) = match options.jsx.as_str() { "react" => (true, false, false), "react-jsx" => (true, true, false), "react-jsxdev" => (true, true, true), _ => (false, false, false), }; EmitOptions { emit_metadata: options.emit_decorator_metadata, imports_not_used_as_values, inline_source_map: options.inline_source_map, inline_sources: options.inline_sources, source_map: options.source_map, jsx_automatic, jsx_development, jsx_factory: options.jsx_factory, jsx_fragment_factory: options.jsx_fragment_factory, jsx_import_source: options.jsx_import_source, transform_jsx, repl_imports: false, } } } fn strip_config_from_emit_options( options: &EmitOptions, ) -> typescript::strip::Config { typescript::strip::Config { pragma: Some(options.jsx_factory.clone()), pragma_frag: Some(options.jsx_fragment_factory.clone()), import_not_used_as_values: match options.imports_not_used_as_values { ImportsNotUsedAsValues::Remove => { typescript::strip::ImportsNotUsedAsValues::Remove } ImportsNotUsedAsValues::Preserve => { typescript::strip::ImportsNotUsedAsValues::Preserve } // `Error` only affects the type-checking stage. Fall back to `Remove` here. ImportsNotUsedAsValues::Error => { typescript::strip::ImportsNotUsedAsValues::Remove } }, use_define_for_class_fields: true, // TODO(bartlomieju): this could be changed to `false` to provide `export {}` // in Typescript files without manual changes no_empty_export: true, } } /// Implements a configuration trait for source maps that reflects the logic /// to embed sources in the source map or not. #[derive(Debug)] pub(crate) struct SourceMapConfig { pub inline_sources: bool, } impl deno_ast::swc::common::source_map::SourceMapGenConfig for SourceMapConfig { fn file_name_to_source(&self, f: &FileName) -> String { f.to_string() } fn inline_sources_content(&self, f: &FileName) -> bool { match f { FileName::Real(..) | FileName::Custom(..) => false, FileName::Url(..) => self.inline_sources, _ => true, } } } /// Transform a TypeScript file into a JavaScript file, based on the supplied /// options. /// /// The result is a tuple of the code and optional source map as strings. pub fn transpile( parsed_source: &ParsedSource, options: &EmitOptions, ) -> Result<(String, Option), AnyError> { ensure_no_fatal_diagnostics(parsed_source.diagnostics().iter())?; let program: Program = (*parsed_source.program()).clone(); let source_map = Rc::new(SourceMap::default()); let source_map_config = SourceMapConfig { inline_sources: options.inline_sources, }; let specifier = resolve_url_or_path(parsed_source.specifier())?; let file_name = FileName::Url(specifier); source_map .new_source_file(file_name, parsed_source.source().text().to_string()); let comments = parsed_source.comments().as_single_threaded(); // needs to be mutable let globals = Globals::new(); deno_ast::swc::common::GLOBALS.set(&globals, || { let top_level_mark = Mark::fresh(Mark::root()); let module = fold_program( program, options, source_map.clone(), &comments, top_level_mark, )?; let mut src_map_buf = vec![]; let mut buf = vec![]; { let writer = Box::new(JsWriter::new( source_map.clone(), "\n", &mut buf, Some(&mut src_map_buf), )); let config = deno_ast::swc::codegen::Config { minify: false }; let mut emitter = deno_ast::swc::codegen::Emitter { cfg: config, comments: Some(&comments), cm: source_map.clone(), wr: writer, }; module.emit_with(&mut emitter)?; } let mut src = String::from_utf8(buf)?; let mut map: Option = None; { let mut buf = Vec::new(); source_map .build_source_map_with_config(&mut src_map_buf, None, source_map_config) .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)) }) } /// A low level function which transpiles a source module into an swc /// SourceFile. pub fn transpile_module( specifier: &ModuleSpecifier, source: &str, media_type: MediaType, options: &EmitOptions, cm: Rc, ) -> Result<(Rc, Module), AnyError> { let source = strip_bom(source); let source = if media_type == MediaType::Json { format!( "export default JSON.parse(`{}`);", source.replace("${", "\\${").replace('`', "\\`") ) } else { source.to_string() }; let source_file = cm.new_source_file(FileName::Url(specifier.clone()), source); let input = StringInput::from(&*source_file); let comments = SingleThreadedComments::default(); let syntax = if media_type == MediaType::Json { get_syntax(MediaType::JavaScript) } else { get_syntax(media_type) }; let lexer = Lexer::new(syntax, deno_ast::ES_VERSION, input, Some(&comments)); let mut parser = deno_ast::swc::parser::Parser::new_from(lexer); let module = parser .parse_module() .map_err(|e| swc_err_to_diagnostic(&cm, specifier, e))?; let diagnostics = parser .take_errors() .into_iter() .map(|e| swc_err_to_diagnostic(&cm, specifier, e)) .collect::>(); ensure_no_fatal_diagnostics(diagnostics.iter())?; let top_level_mark = Mark::fresh(Mark::root()); let program = fold_program( Program::Module(module), options, cm, &comments, top_level_mark, )?; let module = match program { Program::Module(module) => module, _ => unreachable!(), }; Ok((source_file, module)) } #[derive(Default, Clone)] struct DiagnosticCollector { diagnostics_cell: Rc>>, } impl DiagnosticCollector { pub fn into_handler(self) -> deno_ast::swc::common::errors::Handler { deno_ast::swc::common::errors::Handler::with_emitter( true, false, Box::new(self), ) } } impl deno_ast::swc::common::errors::Emitter for DiagnosticCollector { fn emit( &mut self, db: &deno_ast::swc::common::errors::DiagnosticBuilder<'_>, ) { use std::ops::Deref; self.diagnostics_cell.borrow_mut().push(db.deref().clone()); } } fn fold_program( program: Program, options: &EmitOptions, source_map: Rc, comments: &SingleThreadedComments, top_level_mark: Mark, ) -> Result { let jsx_pass = react::react( source_map.clone(), Some(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, runtime: if options.jsx_automatic { Some(react::Runtime::Automatic) } else { None }, development: options.jsx_development, import_source: options.jsx_import_source.clone().unwrap_or_default(), ..Default::default() }, top_level_mark, ); let mut passes = chain!( Optional::new(transforms::DownlevelImportsFolder, options.repl_imports), Optional::new(transforms::StripExportsFolder, options.repl_imports), proposals::decorators::decorators(proposals::decorators::Config { legacy: true, emit_metadata: options.emit_metadata }), helpers::inject_helpers(), resolver_with_mark(top_level_mark), Optional::new( typescript::strip::strip_with_config( strip_config_from_emit_options(options), top_level_mark ), !options.transform_jsx ), Optional::new( typescript::strip::strip_with_jsx( source_map.clone(), strip_config_from_emit_options(options), comments, top_level_mark ), options.transform_jsx ), Optional::new(jsx_pass, options.transform_jsx), fixer(Some(comments)), hygiene(), ); let emitter = DiagnosticCollector::default(); let diagnostics_cell = emitter.diagnostics_cell.clone(); let handler = emitter.into_handler(); let result = deno_ast::swc::utils::HANDLER.set(&handler, || { helpers::HELPERS.set(&helpers::Helpers::new(false), || { program.fold_with(&mut passes) }) }); let diagnostics = diagnostics_cell.borrow(); ensure_no_fatal_swc_diagnostics(&source_map, diagnostics.iter())?; Ok(result) } fn ensure_no_fatal_swc_diagnostics<'a>( source_map: &SourceMap, diagnostics: impl Iterator, ) -> Result<(), AnyError> { let fatal_diagnostics = diagnostics .filter(|d| is_fatal_swc_diagnostic(d)) .collect::>(); if !fatal_diagnostics.is_empty() { Err(anyhow!( "{}", fatal_diagnostics .iter() .map(|d| format_swc_diagnostic(source_map, d)) .collect::>() .join("\n\n") )) } else { Ok(()) } } fn is_fatal_swc_diagnostic(diagnostic: &SwcDiagnostic) -> bool { use deno_ast::swc::common::errors::Level; match diagnostic.level { Level::Bug | Level::Cancelled | Level::FailureNote | Level::Fatal | Level::PhaseFatal | Level::Error => true, Level::Help | Level::Note | Level::Warning => false, } } fn format_swc_diagnostic( source_map: &SourceMap, diagnostic: &SwcDiagnostic, ) -> String { if let Some(span) = &diagnostic.span.primary_span() { let file_name = source_map.span_to_filename(*span); let loc = source_map.lookup_char_pos(span.lo); format!( "{} at {}:{}:{}", diagnostic.message(), file_name.to_string(), loc.line, loc.col_display + 1, ) } else { diagnostic.message() } } fn swc_err_to_diagnostic( source_map: &SourceMap, specifier: &ModuleSpecifier, err: SwcError, ) -> Diagnostic { let location = source_map.lookup_char_pos(err.span().lo); Diagnostic { specifier: specifier.to_string(), span: err.span(), display_position: LineAndColumnDisplay { line_number: location.line, column_number: location.col_display + 1, }, kind: err.into_kind(), } } fn ensure_no_fatal_diagnostics<'a>( diagnostics: impl Iterator, ) -> Result<(), Diagnostics> { let fatal_diagnostics = diagnostics .filter(|d| is_fatal_syntax_error(&d.kind)) .map(ToOwned::to_owned) .collect::>(); if !fatal_diagnostics.is_empty() { Err(Diagnostics(fatal_diagnostics)) } else { Ok(()) } } fn is_fatal_syntax_error(error_kind: &SyntaxError) -> bool { matches!( error_kind, // expected identifier SyntaxError::TS1003 | // expected semi-colon SyntaxError::TS1005 | // expected expression SyntaxError::TS1109 | // unterminated string literal SyntaxError::UnterminatedStrLit ) } #[cfg(test)] mod tests { use super::*; use deno_ast::parse_module; use deno_ast::ParseParams; use deno_ast::SourceTextInfo; use pretty_assertions::assert_eq; #[test] fn test_transpile() { let specifier = resolve_url_or_path("https://deno.land/x/mod.ts") .expect("could not resolve specifier"); let source = r#" enum D { A, B, } namespace N { export enum D { A = "value" } export const Value = 5; } 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; console.log(N.Value); } } "#; let module = deno_ast::parse_module(ParseParams { specifier: specifier.as_str().to_string(), source: SourceTextInfo::from_string(source.to_string()), media_type: deno_ast::MediaType::TypeScript, capture_tokens: false, maybe_syntax: None, scope_analysis: false, }) .unwrap(); let (code, maybe_map) = transpile(&module, &EmitOptions::default()) .expect("could not strip types"); let expected_text = r#"var D; (function(D) { D[D["A"] = 0] = "A"; D[D["B"] = 1] = "B"; })(D || (D = { })); var N; (function(N1) { let D; (function(D) { D["A"] = "value"; })(D = N1.D || (N1.D = { })); N1.Value = 5; })(N || (N = { })); export class A { d; b; c = 1; e; constructor(d = D.A){ this.d = d; const e = "foo"; this.e = e; console.log(N.Value); } } "#; assert_eq!(&code[..expected_text.len()], expected_text); assert!( code.contains("\n//# sourceMappingURL=data:application/json;base64,") ); assert!(maybe_map.is_none()); } #[test] fn test_transpile_tsx() { let specifier = resolve_url_or_path("https://deno.land/x/mod.ts") .expect("could not resolve specifier"); let source = r#" export class A { render() { return
} } "#; let module = parse_module(ParseParams { specifier: specifier.as_str().to_string(), source: SourceTextInfo::from_string(source.to_string()), media_type: deno_ast::MediaType::Tsx, capture_tokens: false, maybe_syntax: None, scope_analysis: true, // ensure scope analysis doesn't conflict with a second resolver pass }) .expect("could not parse module"); let (code, _) = transpile(&module, &EmitOptions::default()) .expect("could not strip types"); assert!(code.contains("React.createElement(\"div\", null")); } #[test] fn test_transpile_jsx_pragma() { let specifier = resolve_url_or_path("https://deno.land/x/mod.ts") .expect("could not resolve specifier"); let source = r#" /** @jsx h */ /** @jsxFrag Fragment */ import { h, Fragment } from "https://deno.land/x/mod.ts"; function App() { return (
<>
); }"#; let module = parse_module(ParseParams { specifier: specifier.as_str().to_string(), source: SourceTextInfo::from_string(source.to_string()), media_type: deno_ast::MediaType::Jsx, capture_tokens: false, maybe_syntax: None, scope_analysis: true, }) .unwrap(); let (code, _) = transpile(&module, &EmitOptions::default()).unwrap(); let expected = r#"/** @jsx h */ /** @jsxFrag Fragment */ import { h, Fragment } from "https://deno.land/x/mod.ts"; function App() { return(/*#__PURE__*/ h("div", null, /*#__PURE__*/ h(Fragment, null))); }"#; assert_eq!(&code[..expected.len()], expected); } #[test] fn test_transpile_jsx_import_source_pragma() { let specifier = resolve_url_or_path("https://deno.land/x/mod.tsx") .expect("could not resolve specifier"); let source = r#" /** @jsxImportSource jsx_lib */ function App() { return (
<>
); }"#; let module = parse_module(ParseParams { specifier: specifier.as_str().to_string(), source: SourceTextInfo::from_string(source.to_string()), media_type: deno_ast::MediaType::Jsx, capture_tokens: false, maybe_syntax: None, scope_analysis: true, }) .unwrap(); let (code, _) = transpile(&module, &EmitOptions::default()).unwrap(); let expected = r#"import { jsx as _jsx, Fragment as _Fragment } from "jsx_lib/jsx-runtime"; /** @jsxImportSource jsx_lib */ function App() { return(/*#__PURE__*/ _jsx("div", { children: /*#__PURE__*/ _jsx(_Fragment, { }) })); "#; assert_eq!(&code[..expected.len()], expected); } #[test] fn test_transpile_jsx_import_source_no_pragma() { let specifier = resolve_url_or_path("https://deno.land/x/mod.tsx") .expect("could not resolve specifier"); let source = r#" function App() { return (
<>
); }"#; let module = parse_module(ParseParams { specifier: specifier.as_str().to_string(), source: SourceTextInfo::from_string(source.to_string()), media_type: deno_ast::MediaType::Jsx, capture_tokens: false, maybe_syntax: None, scope_analysis: true, }) .unwrap(); let emit_options = EmitOptions { jsx_automatic: true, jsx_import_source: Some("jsx_lib".to_string()), ..Default::default() }; let (code, _) = transpile(&module, &emit_options).unwrap(); let expected = r#"import { jsx as _jsx, Fragment as _Fragment } from "jsx_lib/jsx-runtime"; function App() { return(/*#__PURE__*/ _jsx("div", { children: /*#__PURE__*/ _jsx(_Fragment, { }) })); } "#; assert_eq!(&code[..expected.len()], expected); } // TODO(@kitsonk) https://github.com/swc-project/swc/issues/2656 // #[test] // fn test_transpile_jsx_import_source_no_pragma_dev() { // let specifier = resolve_url_or_path("https://deno.land/x/mod.tsx") // .expect("could not resolve specifier"); // let source = r#" // function App() { // return ( //
<>
// ); // }"#; // let module = parse_module(ParseParams { // specifier: specifier.as_str().to_string(), // source: SourceTextInfo::from_string(source.to_string()), // media_type: deno_ast::MediaType::Jsx, // capture_tokens: false, // maybe_syntax: None, // scope_analysis: true, // }) // .unwrap(); // let emit_options = EmitOptions { // jsx_automatic: true, // jsx_import_source: Some("jsx_lib".to_string()), // jsx_development: true, // ..Default::default() // }; // let (code, _) = transpile(&module, &emit_options).unwrap(); // let expected = r#"import { jsx as _jsx, Fragment as _Fragment } from "jsx_lib/jsx-dev-runtime"; // function App() { // return(/*#__PURE__*/ _jsx("div", { // children: /*#__PURE__*/ _jsx(_Fragment, { // }) // })); // } // "#; // assert_eq!(&code[..expected.len()], expected); // } #[test] fn test_transpile_decorators() { let specifier = resolve_url_or_path("https://deno.land/x/mod.ts") .expect("could not resolve specifier"); 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 module = parse_module(ParseParams { specifier: specifier.as_str().to_string(), source: SourceTextInfo::from_string(source.to_string()), media_type: deno_ast::MediaType::TypeScript, capture_tokens: false, maybe_syntax: None, scope_analysis: false, }) .expect("could not parse module"); let (code, _) = transpile(&module, &EmitOptions::default()) .expect("could not strip types"); assert!(code.contains("_applyDecoratedDescriptor(")); } #[test] fn transpile_handle_code_nested_in_ts_nodes_with_jsx_pass() { // from issue 12409 let specifier = resolve_url_or_path("https://deno.land/x/mod.ts").unwrap(); let source = r#" export function g() { let algorithm: any algorithm = {} return ( test(algorithm, false, keyUsages) ) } "#; let module = parse_module(ParseParams { specifier: specifier.as_str().to_string(), source: SourceTextInfo::from_string(source.to_string()), media_type: deno_ast::MediaType::TypeScript, capture_tokens: false, maybe_syntax: None, scope_analysis: false, }) .unwrap(); let emit_options = EmitOptions { transform_jsx: true, ..Default::default() }; let (code, _) = transpile(&module, &emit_options).unwrap(); let expected = r#"export function g() { let algorithm; algorithm = { }; return test(algorithm, false, keyUsages); }"#; assert_eq!(&code[..expected.len()], expected); } #[test] fn diagnostic_jsx_spread_instead_of_panic() { let specifier = resolve_url_or_path("https://deno.land/x/mod.ts").unwrap(); let source = r#"const A = () => { return
{...[]}
; };"#; let parsed_source = parse_module(ParseParams { specifier: specifier.as_str().to_string(), source: SourceTextInfo::from_string(source.to_string()), media_type: deno_ast::MediaType::Tsx, capture_tokens: false, maybe_syntax: None, scope_analysis: false, }) .unwrap(); let err = transpile(&parsed_source, &Default::default()) .err() .unwrap(); assert_eq!(err.to_string(), "Spread children are not supported in React. at https://deno.land/x/mod.ts:2:15"); } }