use anyhow::{Error, Result};
use std::{collections::HashMap, sync::Arc};
use swc_bundler::{Bundler, Hook, Load, ModuleData, ModuleRecord};
use swc_common::{
errors::{ColorConfig, Handler},
FileName, Globals, Mark, SourceMap, Span, GLOBALS,
};
use swc_ecma_ast::*;
use swc_ecma_codegen::{
text_writer::{omit_trailing_semi, JsWriter, WriteJs},
Emitter,
};
use swc_ecma_loader::{
resolvers::{lru::CachingResolver, node::NodeModulesResolver},
TargetEnv,
};
use swc_ecma_parser::{parse_file_as_module, Syntax, TsConfig};
use swc_ecma_transforms_base::fixer::fixer;
use swc_ecma_visit::{FoldWith, VisitMutWith};
use std::{fs::write, path::Path};
pub fn bundle_ts(path: &str) -> Result<()> {
let minify = !cfg!(debug_assertions);
let tree_shaking = false;
let js = swc_run(path, minify, tree_shaking)?;
let ts_filename = Path::new(path).file_name().unwrap().to_str().unwrap();
let js_filename = ts_filename.replace(".ts", ".js").replace(".tsx", ".js");
let out_file = super::out_path(&js_filename);
write(out_file, js)?;
Ok(())
}
fn swc_run(main: &str, minify: bool, tree_shaking: bool) -> Result<String, Error> {
let mut entries = HashMap::default();
entries.insert("main".into(), swc_common::FileName::Real(main.into()));
let source_map_rc = Arc::new(SourceMap::default());
let loader = Loader(source_map_rc.clone());
let resolver = CachingResolver::new(
4096,
NodeModulesResolver::new(TargetEnv::Browser, Default::default(), true),
);
let hook = Box::new(ImportMetaProps);
let bundler_config = swc_bundler::Config {
require: false,
disable_inliner: true,
external_modules: Default::default(),
disable_fixer: minify,
disable_hygiene: minify,
disable_dce: !tree_shaking,
module: Default::default(),
};
use swc_ecma_minifier::option::{
CompressOptions, MangleOptions, MinifyOptions, TopLevelOptions,
};
let minify_options = &MinifyOptions {
compress: Some(CompressOptions {
top_level: Some(TopLevelOptions { functions: true }),
..Default::default()
}),
mangle: Some(MangleOptions {
top_level: Some(true),
..Default::default()
}),
..Default::default()
};
let code = GLOBALS.set(&Globals::new(), || {
let globals = Box::leak(Box::new(Globals::default()));
let mut bundler = Bundler::new(
globals,
source_map_rc.clone(),
loader,
resolver,
bundler_config,
hook,
);
let mut bundles = bundler
.bundle(entries)
.map_err(|err| println!("{:?}", err))
.expect("should bundle stuff");
if minify {
let minify_extra_options = &swc_ecma_minifier::option::ExtraOptions {
unresolved_mark: Mark::new(),
top_level_mark: Mark::new(),
};
bundles = bundles
.into_iter()
.map(|mut b| {
GLOBALS.set(globals, || {
b.module = swc_ecma_minifier::optimize(
b.module.into(),
source_map_rc.clone(),
None,
None,
&minify_options,
&minify_extra_options,
)
.expect_module();
b.module.visit_mut_with(&mut fixer(None));
b
})
})
.collect();
}
bundles = bundles
.into_iter()
.map(|mut b| {
GLOBALS.set(globals, || {
b.module = Into::<Program>::into(b.module)
.fold_with(&mut swc_ecma_transforms_typescript::strip(Mark::new()))
.expect_module();
b.module.visit_mut_with(&mut fixer(None));
b
})
})
.collect();
let bundled = &bundles[0];
let mut buf = vec![];
{
let writer = JsWriter::new(source_map_rc.clone(), "\n", &mut buf, None);
let mut emitter = Emitter {
cfg: swc_ecma_codegen::Config::default().with_minify(minify),
cm: source_map_rc.clone(),
comments: None,
wr: if minify {
Box::new(omit_trailing_semi(writer)) as Box<dyn WriteJs>
} else {
Box::new(writer) as Box<dyn WriteJs>
},
};
emitter.emit_module(&bundled.module).unwrap();
}
let code = String::from_utf8_lossy(&buf).to_string();
code
});
Ok(code)
}
struct ImportMetaProps;
impl Hook for ImportMetaProps {
fn get_import_meta_props(
&self,
_span: Span,
_module_record: &ModuleRecord,
) -> Result<Vec<swc_ecma_ast::KeyValueProp>, anyhow::Error> {
Ok(vec![])
}
}
pub struct Loader(Arc<SourceMap>);
impl Load for Loader {
fn load(&self, f: &FileName) -> Result<ModuleData, Error> {
let fm = match f {
FileName::Real(path) => self.0.load_file(path)?,
_ => unreachable!(),
};
let module = parse_file_as_module(
&fm,
Syntax::Typescript(TsConfig::default()),
EsVersion::Es2020,
None,
&mut vec![],
)
.unwrap_or_else(|err| {
let handler =
Handler::with_tty_emitter(ColorConfig::Always, true, false, Some(self.0.clone()));
err.into_diagnostic(&handler).emit();
panic!(
"failed to parse(load) file {}",
match f {
FileName::Real(path) => path.to_str().unwrap(),
_ => unreachable!(),
}
)
});
Ok(ModuleData {
fm,
module,
helpers: Default::default(),
})
}
}