use std::hash::Hasher;
use std::path::Path;
use std::path::PathBuf;
use anyhow::Context;
use manganis_core::JsAssetOptions;
use swc_common::errors::Emitter;
use swc_common::errors::Handler;
use swc_common::input::SourceFileInput;
use swc_ecma_minifier::option::{ExtraOptions, MinifyOptions};
use swc_ecma_parser::lexer::Lexer;
use swc_ecma_parser::Parser;
use swc_ecma_transforms_base::fixer::fixer;
use swc_ecma_visit::VisitMutWith;
use std::collections::HashMap;
use anyhow::Error;
use swc_bundler::{Bundler, Config, Load, ModuleData, ModuleRecord};
use swc_common::{
errors::HANDLER, sync::Lrc, FileName, FilePathMapping, Globals, Mark, SourceMap, Span, GLOBALS,
};
use swc_ecma_ast::*;
use swc_ecma_codegen::text_writer::JsWriter;
use swc_ecma_loader::{resolvers::node::NodeModulesResolver, TargetEnv};
use swc_ecma_parser::{parse_file_as_module, Syntax};
use crate::hash::hash_file_contents;
struct TracingEmitter;
impl Emitter for TracingEmitter {
fn emit(&mut self, db: &mut swc_common::errors::DiagnosticBuilder<'_>) {
match db.level {
swc_common::errors::Level::Bug
| swc_common::errors::Level::Fatal
| swc_common::errors::Level::PhaseFatal
| swc_common::errors::Level::Error => tracing::error!("{}", db.message()),
swc_common::errors::Level::Warning
| swc_common::errors::Level::FailureNote
| swc_common::errors::Level::Cancelled => tracing::warn!("{}", db.message()),
swc_common::errors::Level::Note | swc_common::errors::Level::Help => {
tracing::trace!("{}", db.message())
}
}
}
}
fn inside_handler<O>(f: impl FnOnce(&Globals, Lrc<SourceMap>) -> O) -> O {
let globals = Globals::new();
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
let handler = Handler::with_emitter_and_flags(Box::new(TracingEmitter), Default::default());
GLOBALS.set(&globals, || HANDLER.set(&handler, || f(&globals, cm)))
}
fn bundle_js_to_writer(
file: PathBuf,
bundle: bool,
minify: bool,
write_to: &mut impl std::io::Write,
) -> anyhow::Result<()> {
inside_handler(|globals, cm| {
bundle_js_to_writer_inside_handler(globals, cm, file, bundle, minify, write_to)
})
}
fn resolve_js_inside_handler(
globals: &Globals,
file: PathBuf,
bundle: bool,
cm: &Lrc<SourceMap>,
) -> anyhow::Result<Module> {
if bundle {
let node_resolver = NodeModulesResolver::new(TargetEnv::Browser, Default::default(), true);
let mut bundler = Bundler::new(
globals,
cm.clone(),
PathLoader { cm: cm.clone() },
node_resolver,
Config {
require: true,
..Default::default()
},
Box::new(Hook),
);
let mut entries = HashMap::default();
entries.insert("main".to_string(), FileName::Real(file));
let mut bundles = bundler
.bundle(entries)
.context("failed to bundle javascript with swc")?;
let bundle = bundles
.pop()
.ok_or_else(|| anyhow::anyhow!("swc did not output any bundles"))?;
Ok(bundle.module)
} else {
let fm = cm.load_file(Path::new(&file)).expect("Failed to load file");
let lexer = Lexer::new(
Default::default(),
Default::default(),
SourceFileInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
parser.parse_module().map_err(|err| {
HANDLER.with(|handler| {
let mut error = err.into_diagnostic(handler);
error.cancel();
anyhow::anyhow!("{}", error.message())
})
})
}
}
fn bundle_js_to_writer_inside_handler(
globals: &Globals,
cm: Lrc<SourceMap>,
file: PathBuf,
bundle: bool,
minify: bool,
write_to: &mut impl std::io::Write,
) -> anyhow::Result<()> {
let mut module = resolve_js_inside_handler(globals, file, bundle, &cm)?;
if minify {
module = swc_ecma_minifier::optimize(
std::mem::take(&mut module).into(),
cm.clone(),
None,
None,
&MinifyOptions {
rename: true,
compress: None,
mangle: None,
..Default::default()
},
&ExtraOptions {
unresolved_mark: Mark::new(),
top_level_mark: Mark::new(),
mangle_name_cache: None,
},
)
.expect_module();
module.visit_mut_with(&mut fixer(None));
}
let mut emitter = swc_ecma_codegen::Emitter {
cfg: swc_ecma_codegen::Config::default().with_minify(minify),
cm: cm.clone(),
comments: None,
wr: Box::new(JsWriter::new(cm, "\n", write_to, None)),
};
emitter.emit_module(&module)?;
Ok(())
}
struct PathLoader {
cm: Lrc<SourceMap>,
}
impl Load for PathLoader {
fn load(&self, file: &FileName) -> anyhow::Result<ModuleData> {
let file = match file {
FileName::Real(v) => v,
_ => anyhow::bail!("Only real files are supported"),
};
let fm = self.cm.load_file(file)?;
let module = HANDLER.with(|handler| {
parse_file_as_module(
&fm,
Syntax::Es(Default::default()),
Default::default(),
None,
&mut Vec::new(),
)
.map_err(|err| {
let mut error = err.into_diagnostic(handler);
error.cancel();
anyhow::anyhow!("{}", error.message())
})
.context("Failed to parse javascript")
})?;
Ok(ModuleData {
fm,
module,
helpers: Default::default(),
})
}
}
struct Hook;
impl swc_bundler::Hook for Hook {
fn get_import_meta_props(
&self,
span: Span,
module_record: &ModuleRecord,
) -> Result<Vec<KeyValueProp>, Error> {
let file_name = module_record.file_name.to_string();
Ok(vec![
KeyValueProp {
key: PropName::Ident(IdentName::new("url".into(), span)),
value: Box::new(Expr::Lit(Lit::Str(Str {
span,
raw: None,
value: file_name.into(),
}))),
},
KeyValueProp {
key: PropName::Ident(IdentName::new("main".into(), span)),
value: Box::new(if module_record.is_entry {
Expr::Member(MemberExpr {
span,
obj: Box::new(Expr::MetaProp(MetaPropExpr {
span,
kind: MetaPropKind::ImportMeta,
})),
prop: MemberProp::Ident(IdentName::new("main".into(), span)),
})
} else {
Expr::Lit(Lit::Bool(Bool { span, value: false }))
}),
},
])
}
}
pub(crate) fn process_js(
js_options: &JsAssetOptions,
source: &Path,
output_path: &Path,
bundle: bool,
) -> anyhow::Result<()> {
let mut writer = std::io::BufWriter::new(std::fs::File::create(output_path)?);
if js_options.minified() {
if let Err(err) = bundle_js_to_writer(source.to_path_buf(), bundle, true, &mut writer) {
tracing::error!("Failed to minify js. Falling back to non-minified: {err}");
} else {
return Ok(());
}
}
let mut source_file = std::fs::File::open(source)?;
std::io::copy(&mut source_file, &mut writer).with_context(|| {
format!(
"Failed to write js to output location: {}",
output_path.display()
)
})?;
Ok(())
}
fn hash_js_module(file: PathBuf, hasher: &mut impl Hasher, bundle: bool) -> anyhow::Result<()> {
inside_handler(|globals, cm| {
_ = resolve_js_inside_handler(globals, file, bundle, &cm)?;
for file in cm.files().iter() {
let hash = file.src_hash;
hasher.write(&hash.to_le_bytes());
}
Ok(())
})
}
pub(crate) fn hash_js(
js_options: &JsAssetOptions,
source: &Path,
hasher: &mut impl Hasher,
bundle: bool,
) -> anyhow::Result<()> {
if js_options.minified() {
if let Err(err) = hash_js_module(source.to_path_buf(), hasher, bundle) {
tracing::error!("Failed to minify js. Falling back to non-minified: {err}");
hash_file_contents(source, hasher)?;
}
} else {
hash_file_contents(source, hasher)?;
}
Ok(())
}