use rolldown_common::{
AddonOutputOption, BundlerOptions, Either, IsExternal, JsxOptions, LegalComments, ModuleType,
Platform, RawMinifyOptions, ResolveOptions, TreeshakeOptions,
};
use rolldown_utils::indexmap::FxIndexMap;
use rolldown_utils::pattern_filter::StringOrRegex;
use rustc_hash::FxHashMap;
use serde_json::{Map, Value};
use crate::optimize::OptimizeInput;
const BASELINE_TARGET: &[&str] = &[
"chrome111",
"edge111",
"firefox114",
"safari16.4",
"ios16.4",
];
const IMPLICIT_CONDITIONS: &[&str] = &["import", "require", "default"];
pub(crate) struct DepOptions {
platform: Platform,
resolve: ResolveOptions,
define: FxIndexMap<String, String>,
target: Either<String, Vec<String>>,
jsx: Option<Either<String, JsxOptions>>,
module_types: FxHashMap<String, ModuleType>,
external: Vec<String>,
treeshake: Option<bool>,
keep_names: Option<bool>,
output: Output,
}
#[derive(Default)]
struct Output {
banner: Option<String>,
footer: Option<String>,
minify: Option<bool>,
legal_comments: Option<LegalComments>,
keep_names: Option<bool>,
top_level_var: Option<bool>,
}
fn strings(v: &Value) -> Option<Vec<String>> {
match v {
Value::String(s) => Some(vec![s.clone()]),
Value::Array(a) => a.iter().map(|s| s.as_str().map(str::to_string)).collect(),
_ => None,
}
}
fn unsupported(path: &str) {
static SEEN: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
let mut seen = SEEN.lock().unwrap_or_else(|e| e.into_inner());
if !seen.iter().any(|p| p == path) {
seen.push(path.to_string());
eprintln!(
"oj: optimizeDeps.{path} is not supported by oj's dependency pre-bundle; ignored"
);
}
}
fn alias(v: &Value) -> Option<Vec<(String, Vec<Option<String>>)>> {
let map = v.as_object()?;
Some(
map.iter()
.map(|(find, to)| {
let targets = match to {
Value::Bool(false) => vec![None],
Value::Array(a) => a.iter().map(|t| t.as_str().map(str::to_string)).collect(),
other => vec![other.as_str().map(str::to_string)],
};
(find.clone(), targets)
})
.collect(),
)
}
fn jsx(v: &Value) -> Option<Either<String, JsxOptions>> {
match v {
Value::String(s) => Some(Either::Left(s.clone())),
Value::Object(o) => {
let s = |k: &str| o.get(k).and_then(|v| v.as_str()).map(str::to_string);
Some(Either::Right(JsxOptions {
runtime: s("runtime"),
development: o.get("development").and_then(Value::as_bool),
import_source: s("importSource"),
pragma: s("pragma"),
pragma_frag: s("pragmaFrag"),
..Default::default()
}))
}
_ => None,
}
}
impl DepOptions {
pub fn new(input: &OptimizeInput) -> DepOptions {
let esbuild = input
.bundler_options
.as_ref()
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
let rolldown = input
.rolldown_options
.as_ref()
.and_then(Value::as_object)
.cloned()
.unwrap_or_default();
let es = |k: &str| esbuild.get(k).filter(|v| !v.is_null());
let conditions: Vec<String> = es("conditions").and_then(strings).unwrap_or_else(|| {
if input.conditions.is_empty() {
["browser", "module", "development"]
.map(str::to_string)
.to_vec()
} else {
input.conditions.clone()
}
});
let mut resolve = ResolveOptions {
main_fields: Some(es("mainFields").and_then(strings).unwrap_or_else(|| {
if input.main_fields.is_empty() {
["browser", "module", "main"].map(str::to_string).to_vec()
} else {
input.main_fields.clone()
}
})),
condition_names: Some(
conditions
.into_iter()
.filter(|c| !IMPLICIT_CONDITIONS.contains(&c.as_str()))
.collect(),
),
extensions: es("resolveExtensions")
.and_then(strings)
.or_else(|| (!input.extensions.is_empty()).then(|| input.extensions.clone())),
symlinks: input.preserve_symlinks.then_some(false),
alias: es("alias").and_then(alias),
..Default::default()
};
let mut define = FxIndexMap::default();
define.insert(
"process.env.NODE_ENV".to_string(),
Value::String(input.node_env.clone()).to_string(),
);
if let Some(map) = es("define").and_then(Value::as_object) {
for (k, v) in map {
if let Some(v) = v.as_str() {
define.insert(k.clone(), v.to_string());
}
}
}
let mut options = DepOptions {
platform: es("platform")
.and_then(Value::as_str)
.and_then(|p| Platform::try_from(p).ok())
.unwrap_or(Platform::Browser),
resolve: ResolveOptions::default(),
define,
target: es("target")
.and_then(strings)
.map(Either::Right)
.unwrap_or_else(|| {
Either::Right(BASELINE_TARGET.iter().map(|t| t.to_string()).collect())
}),
jsx: None,
module_types: FxHashMap::default(),
external: es("external").and_then(strings).unwrap_or_default(),
treeshake: None,
keep_names: None,
output: Output::default(),
};
for (key, value) in &rolldown {
match key.as_str() {
"input" | "plugins" => {}
"platform" => match value.as_str().and_then(|p| Platform::try_from(p).ok()) {
Some(p) => options.platform = p,
None => unsupported("rolldownOptions.platform"),
},
"resolve" => apply_resolve(&mut resolve, value),
"transform" => options.apply_transform(value),
"moduleTypes" => {
for (ext, ty) in value.as_object().into_iter().flatten() {
match ty.as_str().map(ModuleType::from_known_str) {
Some(Ok(ty)) => {
options.module_types.insert(ext.clone(), ty);
}
_ => unsupported(&format!("rolldownOptions.moduleTypes[{ext:?}]")),
}
}
}
"external" => match strings(value) {
Some(list) => options.external = list,
None => unsupported("rolldownOptions.external (only names)"),
},
"treeshake" => match value.as_bool() {
Some(b) => options.treeshake = Some(b),
None => unsupported("rolldownOptions.treeshake (only a boolean)"),
},
"keepNames" => options.keep_names = value.as_bool(),
"output" => options.output = Output::from(value),
other => unsupported(&format!("rolldownOptions.{other}")),
}
}
options.resolve = resolve;
options
}
fn apply_transform(&mut self, value: &Value) {
for (key, v) in value.as_object().into_iter().flatten() {
match key.as_str() {
"define" => {
for (k, d) in v.as_object().into_iter().flatten() {
if let Some(d) = d.as_str() {
self.define.insert(k.clone(), d.to_string());
}
}
}
"target" => match strings(v) {
Some(t) => self.target = Either::Right(t),
None => unsupported("rolldownOptions.transform.target"),
},
"jsx" => self.jsx = jsx(v),
other => unsupported(&format!("rolldownOptions.transform.{other}")),
}
}
}
pub fn bundler_options(&self, scan: bool) -> BundlerOptions {
let jsx = self.jsx.clone().or_else(|| {
scan.then(|| {
Either::Right(JsxOptions {
runtime: Some("automatic".into()),
development: Some(true),
..Default::default()
})
})
});
BundlerOptions {
platform: Some(self.platform),
resolve: Some(self.resolve.clone()),
define: (!scan).then(|| self.define.clone()),
transform: Some(rolldown_common::BundlerTransformOptions {
target: (!scan).then(|| self.target.clone()),
jsx,
..Default::default()
}),
module_types: Some(self.module_types.clone()),
external: (!self.external.is_empty()).then(|| {
IsExternal::StringOrRegex(
self.external
.iter()
.cloned()
.map(StringOrRegex::String)
.collect(),
)
}),
treeshake: self
.treeshake
.map(TreeshakeOptions::Boolean)
.unwrap_or_default(),
keep_names: self.output.keep_names.or(self.keep_names),
banner: self
.output
.banner
.clone()
.map(|b| AddonOutputOption::String(Some(b))),
footer: self
.output
.footer
.clone()
.map(|f| AddonOutputOption::String(Some(f))),
minify: self.output.minify.map(RawMinifyOptions::Bool),
legal_comments: self.output.legal_comments,
top_level_var: (!scan).then(|| self.output.top_level_var.unwrap_or(true)),
..Default::default()
}
}
}
fn apply_resolve(resolve: &mut ResolveOptions, value: &Value) {
let list = strings;
let nested =
|v: &Value| -> Option<Vec<Vec<String>>> { v.as_array()?.iter().map(strings).collect() };
for (key, v) in value.as_object().into_iter().flatten() {
let ok = match key.as_str() {
"alias" => alias(v).map(|a| resolve.alias = Some(a)).is_some(),
"aliasFields" => nested(v).map(|f| resolve.alias_fields = Some(f)).is_some(),
"conditionNames" => list(v).map(|c| resolve.condition_names = Some(c)).is_some(),
"exportsFields" => nested(v)
.map(|f| resolve.exports_fields = Some(f))
.is_some(),
"extensions" => list(v).map(|e| resolve.extensions = Some(e)).is_some(),
"mainFields" => list(v).map(|m| resolve.main_fields = Some(m)).is_some(),
"mainFiles" => list(v).map(|m| resolve.main_files = Some(m)).is_some(),
"modules" => list(v).map(|m| resolve.modules = Some(m)).is_some(),
"symlinks" => v.as_bool().map(|s| resolve.symlinks = Some(s)).is_some(),
_ => false,
};
if !ok {
unsupported(&format!("rolldownOptions.resolve.{key}"));
}
}
}
impl From<&Value> for Output {
fn from(value: &Value) -> Output {
let mut out = Output::default();
let empty = Map::new();
for (key, v) in value.as_object().unwrap_or(&empty) {
let ok = match key.as_str() {
"format" | "dir" | "entryFileNames" | "chunkFileNames" | "sourcemap" => true,
"banner" => v.as_str().map(|b| out.banner = Some(b.into())).is_some(),
"footer" => v.as_str().map(|f| out.footer = Some(f.into())).is_some(),
"minify" => v.as_bool().map(|m| out.minify = Some(m)).is_some(),
"keepNames" => v.as_bool().map(|k| out.keep_names = Some(k)).is_some(),
"topLevelVar" => v.as_bool().map(|t| out.top_level_var = Some(t)).is_some(),
"legalComments" => match v.as_str() {
Some("none") => {
out.legal_comments = Some(LegalComments::None);
true
}
Some("inline") => {
out.legal_comments = Some(LegalComments::Inline);
true
}
_ => false,
},
_ => false,
};
if !ok {
unsupported(&format!("rolldownOptions.output.{key}"));
}
}
out
}
}