use anyhow::Error;
use std::{collections::HashMap, path::Path};
use swc_core::{
common::{sync::Lrc, FilePathMapping, Globals, SourceMap, GLOBALS},
ecma::visit::{as_folder, FoldWith},
};
mod common;
mod transforms;
pub(crate) fn bundle(
root: &Path,
target: &Path,
apis_blob_url: String,
dependency_map: &HashMap<String, String>,
) -> Result<String, Error> {
let globals = Globals::default();
let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
let module = common::bundle_into_raw_module(
root,
target,
dependency_map,
&globals,
cm.clone(),
)?;
let code = GLOBALS.set(&globals, || {
let mut rename_apis = as_folder(transforms::ApisImportRenamer(apis_blob_url));
let module = module.fold_with(&mut rename_apis);
let mut buf = vec![];
common::emit_module_to_buf(module, cm.clone(), &mut buf);
String::from_utf8_lossy(&buf).to_string()
});
Ok(code)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::{assert_err_eq, ChainReason};
use pretty_assertions::assert_eq;
use rstest::rstest;
use std::{
fs::{create_dir, read_to_string},
path::PathBuf,
};
use tempfile::{tempdir, TempDir};
fn fixture_dir() -> PathBuf {
Path::new("tests/fixtures/bundler").canonicalize().unwrap()
}
fn setup_temp_dir() -> TempDir {
let temp_dir = tempdir().unwrap();
create_dir(temp_dir.path().join("input")).unwrap();
temp_dir
}
#[rstest]
#[case::jsx_runtime("jsx_runtime", "index.jsx")]
#[case::import("import", "index.jsx")]
#[case::strip_types("strip_types", "index.tsx")]
#[case::default_deps("default_deps", "index.js")]
fn test_bundle_ok(#[case] case: &str, #[case] entry: &str) {
let case_dir = fixture_dir().join(case);
let bundle_root = case_dir.join("input");
let result = bundle(
&bundle_root,
&bundle_root.join(entry),
"blob://dummy-url".to_string(),
&Default::default(),
)
.expect("Expected bundling to succeed");
let expected = read_to_string(case_dir.join("output.js")).unwrap();
assert_eq!(result, expected);
}
#[rstest]
#[case::import_node_modules(
"import_node_modules",
vec![
ChainReason::Skip,
ChainReason::Skip,
ChainReason::Regex("failed to resolve os-name from".to_string()),
ChainReason::Exact(
"node_modules imports should be explicitly included in package.json to \
avoid being bundled at runtime; URL imports are not supported, one \
should vendor its source to local and use a relative import instead"
.to_string()
),
]
)]
#[case::import_url(
"import_url",
vec![
ChainReason::Skip,
ChainReason::Skip,
ChainReason::Regex("failed to resolve https://foo.js from".to_string()),
ChainReason::Exact(
"node_modules imports should be explicitly included in package.json to \
avoid being bundled at runtime; URL imports are not supported, one \
should vendor its source to local and use a relative import instead"
.to_string()
),
]
)]
#[case::import_beyond_root(
"import_beyond_root",
vec![
ChainReason::Skip,
ChainReason::Skip,
ChainReason::Regex("failed to resolve ../../foo from".to_string()),
ChainReason::Regex("Relative imports should not go beyond the root".to_string()),
]
)]
#[case::entry_not_exist(
"entry_not_exist",
vec![ChainReason::Regex("Entry point does not exist".to_string())],
)]
#[case::bad_syntax(
"bad_syntax",
vec![
ChainReason::Skip,
ChainReason::Skip,
ChainReason::Skip,
ChainReason::Regex("error: Expected ';', '}' or <eof>".to_string()),
]
)]
fn test_bundle_error(#[case] case: &str, #[case] expected_error: Vec<ChainReason>) {
let case_dir = fixture_dir().join(case);
let bundle_root = case_dir.join("input");
let error = bundle(
&bundle_root,
&bundle_root.join("index.jsx"),
Default::default(),
&Default::default(),
)
.expect_err("Expected bundling error");
assert_err_eq(error, expected_error);
}
#[rstest]
#[should_panic]
fn test_bundle_import_meta_panic() {
let bundle_root = fixture_dir().join("import_meta/input");
let _ = bundle(
&bundle_root,
&bundle_root.join("index.jsx"),
Default::default(),
&Default::default(),
);
}
#[rstest]
fn test_bundle_absolute_import_error() {
let temp_dir = setup_temp_dir();
let bundle_root = temp_dir.path().join("input");
let index_path = bundle_root.join("index.jsx");
let utils_path = bundle_root.join("utils.js");
std::fs::write(
&index_path,
format!("import {{ foo }} from {utils_path:?}; console.log(foo);"),
)
.unwrap();
std::fs::write(&utils_path, "export const foo = 42;").unwrap();
let error =
bundle(&bundle_root, &index_path, Default::default(), &Default::default())
.expect_err("Expected bundling error");
let expected_error = vec![
ChainReason::Skip,
ChainReason::Skip,
ChainReason::Skip,
ChainReason::Exact(
"Absolute imports are not supported; use relative imports instead"
.to_string(),
),
];
assert_err_eq(error, expected_error);
}
}