use swc_core::common::comments::SingleThreadedComments;
use swc_core::common::source_map::SourceMapGenConfig;
use swc_core::common::sync::Lrc;
use swc_core::common::FileName;
use swc_core::common::Globals;
use swc_core::common::Mark;
use swc_core::common::SourceFile;
use swc_core::common::SourceMap;
use swc_core::common::GLOBALS;
use swc_core::ecma::ast::EsVersion;
use swc_core::ecma::ast::Program;
use swc_core::ecma::codegen::text_writer::JsWriter;
use swc_core::ecma::codegen::Config as CodegenConfig;
use swc_core::ecma::codegen::Emitter;
use swc_core::ecma::parser::lexer::Lexer;
use swc_core::ecma::parser::Parser;
use swc_core::ecma::parser::StringInput;
use swc_core::ecma::parser::Syntax;
use swc_core::ecma::parser::TsSyntax;
use swc_core::ecma::transforms::base::fixer::fixer;
use swc_core::ecma::transforms::base::helpers::inject_helpers;
use swc_core::ecma::transforms::base::helpers::Helpers;
use swc_core::ecma::transforms::base::helpers::HELPERS;
use swc_core::ecma::transforms::base::hygiene::hygiene;
use swc_core::ecma::transforms::base::resolver;
use swc_core::ecma::transforms::proposal::decorator_2022_03::decorator_2022_03;
use swc_core::ecma::transforms::react::react;
use swc_core::ecma::transforms::react::Options as ReactOptions;
use swc_core::ecma::transforms::react::Runtime;
use swc_core::ecma::transforms::typescript::tsx as tsx_strip;
use swc_core::ecma::transforms::typescript::typescript;
use swc_core::ecma::transforms::typescript::Config as TsConfig;
use swc_core::ecma::transforms::typescript::TsxConfig;
use super::helpers::rewrite_imports;
use super::tsconfig::JsxMode;
use super::TransformerContext;
use super::TransformerResult;
#[derive(Debug, Clone, Default)]
struct JsxConfig {
runtime: Option<Runtime>,
pragma: Option<String>,
pragma_frag: Option<String>,
import_source: Option<String>,
development: bool,
preserve: bool,
}
fn jsx_config_from_tsconfig(ctx: &TransformerContext) -> JsxConfig {
let tsconfig = &ctx.tsconfig;
let classic = || JsxConfig {
runtime: Some(Runtime::Classic),
pragma: tsconfig.jsx_factory.clone(),
pragma_frag: tsconfig.jsx_fragment_factory.clone(),
..JsxConfig::default()
};
let automatic = |development: bool| JsxConfig {
runtime: Some(Runtime::Automatic),
import_source: tsconfig.jsx_import_source.clone(),
development,
..JsxConfig::default()
};
match tsconfig.jsx {
Some(JsxMode::React) => classic(),
Some(JsxMode::ReactJsx) => automatic(false),
Some(JsxMode::ReactJsxDev) => automatic(true),
Some(JsxMode::Preserve) | Some(JsxMode::ReactNative) => JsxConfig {
preserve: true,
..JsxConfig::default()
},
None => classic(),
}
}
fn parse(
fm: &Lrc<SourceFile>,
comments: &SingleThreadedComments,
tsx: bool,
) -> Result<Program, Vec<String>> {
let lexer = Lexer::new(
Syntax::Typescript(TsSyntax {
tsx,
decorators: true,
..TsSyntax::default()
}),
EsVersion::latest(),
StringInput::from(&**fm),
Some(comments),
);
let mut parser = Parser::new_from(lexer);
let program = parser.parse_program();
let mut errors: Vec<String> = parser
.take_errors()
.into_iter()
.map(|error| error.kind().msg().to_string())
.collect();
match program {
Ok(program) if errors.is_empty() => Ok(program),
Ok(_) => Err(errors),
Err(error) => {
errors.push(error.kind().msg().to_string());
Err(errors)
}
}
}
struct SourceMapConfig {
source_url: String,
}
impl SourceMapGenConfig for SourceMapConfig {
fn file_name_to_source(
&self,
_: &FileName,
) -> String {
self.source_url.clone()
}
fn inline_sources_content(
&self,
_: &FileName,
) -> bool {
true
}
}
pub fn transpile(ctx: TransformerContext) -> anyhow::Result<TransformerResult> {
let jsx_config = jsx_config_from_tsconfig(&ctx);
let source = String::from_utf8(ctx.content)?;
let is_tsx = match ctx.kind.as_str() {
"ts" => false,
"tsx" => true,
_ => {
anyhow::bail!("Got an invalid file type");
}
};
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(FileName::Real(ctx.path.clone()).into(), source.clone());
let comments = SingleThreadedComments::default();
let program = match parse(&fm, &comments, is_tsx) {
Ok(program) => program,
Err(errors) => {
if !is_tsx {
let looks_like_jsx =
source.contains("</") || source.contains("/>") || source.contains("<>");
if looks_like_jsx && parse(&fm, &comments, true).is_ok() {
anyhow::bail!(
"JSX syntax detected in a .ts file: {}. Rename the file to .tsx or remove JSX.",
ctx.path.display()
);
}
}
anyhow::bail!("Parse errors: {}", errors.join(", "));
}
};
let globals = Globals::default();
let external_helpers = program.is_module();
let (code, map) = GLOBALS.set(&globals, || {
HELPERS.set(
&Helpers::new(external_helpers),
|| -> anyhow::Result<(String, Option<String>)> {
let unresolved_mark = Mark::new();
let top_level_mark = Mark::new();
let mut program = program.apply(resolver(unresolved_mark, top_level_mark, true));
program = program.apply(decorator_2022_03());
let ts_config = TsConfig {
native_class_properties: true,
..TsConfig::default()
};
if is_tsx && !jsx_config.preserve {
program = program.apply(tsx_strip(
cm.clone(),
ts_config,
TsxConfig {
pragma: Some(
jsx_config
.pragma
.clone()
.unwrap_or_else(|| "React.createElement".to_string())
.into(),
),
pragma_frag: Some(
jsx_config
.pragma_frag
.clone()
.unwrap_or_else(|| "React.Fragment".to_string())
.into(),
),
},
comments.clone(),
unresolved_mark,
top_level_mark,
));
program = program.apply(react(
cm.clone(),
Some(comments.clone()),
ReactOptions {
runtime: Some(jsx_config.runtime.unwrap_or(Runtime::Classic)),
import_source: jsx_config.import_source.clone().map(|value| value.into()),
pragma: jsx_config.pragma.clone().map(|value| value.into()),
pragma_frag: jsx_config.pragma_frag.clone().map(|value| value.into()),
development: Some(jsx_config.development),
..ReactOptions::default()
},
top_level_mark,
unresolved_mark,
));
} else {
program = program.apply(typescript(ts_config, unresolved_mark, top_level_mark));
}
program = program.apply(inject_helpers(unresolved_mark));
program = program.apply(hygiene());
program = program.apply(fixer(Some(&comments)));
if external_helpers {
rewrite_imports(&mut program);
}
let mut code = Vec::new();
let mut mappings = ctx.source_maps.then(Vec::new);
{
let mut emitter = Emitter {
cfg: CodegenConfig::default(),
cm: cm.clone(),
comments: Some(&comments),
wr: JsWriter::new(cm.clone(), "\n", &mut code, mappings.as_mut()),
};
emitter.emit_program(&program)?;
}
let map = match mappings {
Some(mappings) => {
let source_map = cm.build_source_map(
&mappings,
None,
SourceMapConfig {
source_url: ctx.source_url.clone(),
},
);
let mut map = Vec::new();
source_map.to_writer(&mut map)?;
Some(String::from_utf8(map)?)
}
None => None,
};
Ok((String::from_utf8(code)?, map))
},
)
})?;
Ok(TransformerResult { code, map })
}