use std::{borrow::Cow, path::Path, sync::Arc};
use arcstr::ArcStr;
use itertools::Either;
use oxc::{transformer::EngineTargets, transformer_plugins::InjectGlobalVariablesConfig};
use rolldown_common::{
AttachDebugInfo, CodeSplittingMode, GlobalsOutputOption, InjectImport, JsxOptions, JsxPreset,
LegalComments, MinifyOptions, ModuleType, NormalizedBundlerOptions, OutputFormat, Platform,
PreserveEntrySignatures, RawTransformOptions, TransformOptions, TreeshakeOptions, TsConfig,
merge_transform_options_with_tsconfig, normalize_optimization_option,
};
use rolldown_error::{BuildDiagnostic, BuildResult, InvalidOptionType};
use rolldown_fs::{OsFileSystem, OxcResolverFileSystem as _};
use rolldown_resolver::Resolver;
use rolldown_utils::ecmascript::is_validate_identifier_name;
use rustc_hash::{FxHashMap, FxHashSet};
use sugar_path::SugarPath;
use crate::{SharedResolver, utils::determine_minify_internal_exports_default};
pub struct PrepareBuildContext {
pub fs: OsFileSystem,
pub resolver: SharedResolver<OsFileSystem>,
pub options: Arc<NormalizedBundlerOptions>,
pub warnings: Vec<BuildDiagnostic>,
}
fn verify_raw_options(raw_options: &crate::BundlerOptions) -> BuildResult<Vec<BuildDiagnostic>> {
let mut warnings: Vec<BuildDiagnostic> = Vec::new();
let mut errors: Vec<BuildDiagnostic> = Vec::new();
if raw_options.dir.is_some() && raw_options.file.is_some() {
warnings.push(
BuildDiagnostic::invalid_option(InvalidOptionType::InvalidOutputDirOption)
.with_severity_warning(),
);
}
if let Some(entity) = raw_options.context.as_ref() {
if !is_validate_identifier_name(entity) {
warnings.push(
BuildDiagnostic::invalid_option(InvalidOptionType::InvalidContext(entity.clone()))
.with_severity_warning(),
);
}
}
if let Some(format @ (OutputFormat::Umd | OutputFormat::Iife)) = raw_options.format {
if matches!(raw_options.code_splitting, Some(CodeSplittingMode::Bool(true))) {
warnings.push(
BuildDiagnostic::invalid_option(InvalidOptionType::UnsupportedCodeSplittingFormat(
format.to_string(),
))
.with_severity_warning(),
);
}
}
if matches!(raw_options.code_splitting, Some(CodeSplittingMode::Bool(false))) {
if let Some(input) = &raw_options.input
&& input.len() > 1
{
errors.push(BuildDiagnostic::invalid_option(
InvalidOptionType::CodeSplittingDisabledWithMultipleInputs,
));
}
if matches!(raw_options.preserve_modules, Some(true)) {
errors.push(BuildDiagnostic::invalid_option(
InvalidOptionType::CodeSplittingDisabledWithPreserveModules,
));
}
if raw_options.manual_code_splitting.is_some() {
errors.push(BuildDiagnostic::invalid_option(
InvalidOptionType::CodeSplittingDisabledWithManualCodeSplitting,
));
}
}
if let Some(manual_code_splitting) = &raw_options.manual_code_splitting {
let has_groups = manual_code_splitting.groups.as_ref().is_some_and(|groups| !groups.is_empty());
if !has_groups {
let mut specified_options = Vec::new();
if manual_code_splitting.min_share_count.is_some() {
specified_options.push("minShareCount".to_string());
}
if manual_code_splitting.min_size.is_some() {
specified_options.push("minSize".to_string());
}
if manual_code_splitting.max_size.is_some() {
specified_options.push("maxSize".to_string());
}
if manual_code_splitting.min_module_size.is_some() {
specified_options.push("minModuleSize".to_string());
}
if manual_code_splitting.max_module_size.is_some() {
specified_options.push("maxModuleSize".to_string());
}
if manual_code_splitting.include_dependencies_recursively.is_some() {
specified_options.push("includeDependenciesRecursively".to_string());
}
if !specified_options.is_empty() {
warnings.push(
BuildDiagnostic::invalid_option(InvalidOptionType::ManualCodeSplittingWithoutGroups(
specified_options,
))
.with_severity_warning(),
);
}
}
if matches!(manual_code_splitting.include_dependencies_recursively, Some(false)) {
if let Some(preserve_signatures) = &raw_options.preserve_entry_signatures {
if matches!(
preserve_signatures,
PreserveEntrySignatures::Strict | PreserveEntrySignatures::ExportsOnly
) {
errors.push(BuildDiagnostic::invalid_option(
InvalidOptionType::IncludeDependenciesRecursivelyWithConflictPreserveEntrySignatures(
preserve_signatures.to_string(),
),
));
}
}
}
}
if errors.is_empty() { Ok(warnings) } else { Err(errors.into()) }
}
#[expect(clippy::too_many_lines)] pub fn prepare_build_context(
mut raw_options: crate::BundlerOptions,
) -> BuildResult<PrepareBuildContext> {
let mut warnings = verify_raw_options(&raw_options)?;
let format = raw_options.format.unwrap_or(crate::OutputFormat::Esm);
let preserve_entry_signatures = if let Some(manual_code_splitting) =
&raw_options.manual_code_splitting
&& matches!(manual_code_splitting.include_dependencies_recursively, Some(false))
&& raw_options.preserve_entry_signatures.is_none()
{
warnings.push(
BuildDiagnostic::invalid_option(
InvalidOptionType::IncludeDependenciesRecursivelyWithImplicitPreserveEntrySignatures,
)
.with_severity_warning(),
);
PreserveEntrySignatures::AllowExtension
} else {
raw_options.preserve_entry_signatures.unwrap_or_default()
};
let platform = raw_options.platform.unwrap_or(match format {
OutputFormat::Cjs => Platform::Node,
OutputFormat::Esm | OutputFormat::Iife | OutputFormat::Umd => Platform::Browser,
});
let raw_minify = raw_options.minify.unwrap_or_default();
let mut raw_define = raw_options.define.unwrap_or_default();
if matches!(platform, Platform::Browser) && !raw_define.contains_key("process.env.NODE_ENV") {
if raw_minify.is_production() {
raw_define.insert("process.env.NODE_ENV".to_string(), "'production'".to_string());
} else {
raw_define.insert("process.env.NODE_ENV".to_string(), "'development'".to_string());
}
}
let define = raw_define.into_iter().collect();
let mut raw_resolve = std::mem::take(&mut raw_options.resolve).unwrap_or_default();
if raw_resolve.condition_names.is_none() && matches!(platform, Platform::Browser | Platform::Node)
{
raw_resolve.condition_names = Some(vec!["module".to_string()]);
}
let mut module_types: FxHashMap<Cow<'static, str>, ModuleType> = FxHashMap::from(
[
("js".into(), ModuleType::Js),
("mjs".into(), ModuleType::Js),
("cjs".into(), ModuleType::Js),
("jsx".into(), ModuleType::Jsx),
("ts".into(), ModuleType::Ts),
("mts".into(), ModuleType::Ts),
("cts".into(), ModuleType::Ts),
("tsx".into(), ModuleType::Tsx),
("json".into(), ModuleType::Json),
("txt".into(), ModuleType::Text),
("css".into(), ModuleType::Css),
]
.into_iter()
.collect(),
);
if let Some(user_defined_loaders) = raw_options.module_types {
user_defined_loaders.into_iter().for_each(|(ext, value)| {
let stripped = ext.strip_prefix('.').map(ToString::to_string).unwrap_or(ext);
module_types.insert(Cow::Owned(stripped), value);
});
}
let globals = raw_options.globals.unwrap_or(GlobalsOutputOption::FxHashMap(FxHashMap::default()));
let generated_code = raw_options.generated_code.unwrap_or_default();
let oxc_inject_global_variables_config = InjectGlobalVariablesConfig::new(
raw_options
.inject
.as_ref()
.map(|raw_injects| {
raw_injects
.iter()
.map(|raw| match raw {
InjectImport::Named { imported, alias, from } => {
oxc::transformer_plugins::InjectImport::named_specifier(
from,
Some(imported),
alias.as_deref().unwrap_or(imported),
)
}
InjectImport::Namespace { alias, from } => {
oxc::transformer_plugins::InjectImport::namespace_specifier(from, alias)
}
})
.collect()
})
.unwrap_or_default(),
);
let mut experimental = raw_options.experimental.unwrap_or_default();
if experimental.dev_mode.is_some() {
experimental.incremental_build = Some(true);
}
if experimental.attach_debug_info.is_none() {
experimental.attach_debug_info = Some(AttachDebugInfo::Simple);
}
let code_splitting = match format {
OutputFormat::Umd | OutputFormat::Iife => CodeSplittingMode::Bool(false),
_ => raw_options.code_splitting.unwrap_or_default(),
};
let out_dir = raw_options.file.as_ref().map_or_else(
|| raw_options.dir.clone().unwrap_or_else(|| "dist".to_string()),
|file| {
Path::new(file.as_str())
.parent()
.map(|parent| parent.to_string_lossy().to_string())
.unwrap_or_default()
},
);
let cwd =
raw_options.cwd.unwrap_or_else(|| std::env::current_dir().expect("Failed to get current dir"));
let mut raw_treeshake = raw_options.treeshake;
if experimental.dev_mode.is_some() {
raw_treeshake = TreeshakeOptions::Boolean(false);
}
let tsconfig = raw_options.tsconfig.map(|tsconfig| tsconfig.with_base(&cwd)).unwrap_or_default();
let yarn_pnp = raw_resolve.yarn_pnp.unwrap_or(false);
let fs = OsFileSystem::new(yarn_pnp);
let resolver = Arc::new(Resolver::new(fs.clone(), cwd.clone(), platform, &tsconfig, raw_resolve));
let transform_options = {
let mut raw_transform_options = raw_options.transform.unwrap_or_default();
let target = match &raw_transform_options.target {
Some(Either::Left(target)) => EngineTargets::from_target(target),
Some(Either::Right(targets)) => EngineTargets::from_target_list(targets),
None => Ok(EngineTargets::default()),
}
.map_err(|message| {
let hint = message
.contains("Invalid target")
.then(|| "Rolldown only supports ES2015 (ES6) and later.".to_owned());
BuildDiagnostic::bundler_initialize_error(message, hint)
})?;
let mut jsx_preset = JsxPreset::Enable;
if let Some(Either::Left(jsx_str)) = &mut raw_transform_options.jsx {
match jsx_str.as_str() {
"react" => {
raw_transform_options.jsx = Some(Either::Right(JsxOptions {
runtime: Some(String::from("classic")),
..Default::default()
}));
}
"react-jsx" => {
raw_transform_options.jsx = Some(Either::Right(JsxOptions::default()));
}
"preserve" => jsx_preset = JsxPreset::Preserve,
"disable" => {
jsx_preset = JsxPreset::Disable;
"preserve".clone_into(jsx_str);
}
_ => {
Err(BuildDiagnostic::bundler_initialize_error(
format!("Invalid jsx option: `{jsx_str}`."),
Some(
"Valid options are `false | 'react' | 'react-jsx' | 'preserve'`, or jsx options."
.to_owned(),
),
))?;
}
}
}
match tsconfig {
ref v @ TsConfig::Manual(ref path) => {
let resolved_tsconfig = resolver.resolve_tsconfig(&path).map_err(|err| {
anyhow::anyhow!("Failed to resolve `tsconfig` option: {}", path.display()).context(err)
})?;
Box::new(if resolved_tsconfig.references_resolved.is_empty() {
TransformOptions::new(
merge_transform_options_with_tsconfig(
raw_transform_options,
Some(&resolved_tsconfig),
&mut warnings,
)?,
target,
jsx_preset,
)
} else {
TransformOptions::new_raw(
RawTransformOptions::new(raw_transform_options, v.clone(), yarn_pnp),
target,
jsx_preset,
)
})
}
v @ TsConfig::Auto(is_auto) => {
Box::new(if is_auto {
TransformOptions::new_raw(
RawTransformOptions::new(raw_transform_options, v, yarn_pnp),
target,
jsx_preset,
)
} else {
TransformOptions::new(
merge_transform_options_with_tsconfig(raw_transform_options, None, &mut warnings)?,
target,
jsx_preset,
)
})
}
}
};
let mut normalized = NormalizedBundlerOptions {
input: raw_options.input.unwrap_or_default(),
external: raw_options.external.unwrap_or_default(),
treeshake: raw_treeshake.into_normalized_options(),
platform,
name: raw_options.name,
entry_filenames: raw_options.entry_filenames.unwrap_or_else(|| "[name].js".to_string().into()),
chunk_filenames: raw_options
.chunk_filenames
.unwrap_or_else(|| "[name]-[hash].js".to_string().into()),
asset_filenames: raw_options
.asset_filenames
.unwrap_or_else(|| "assets/[name]-[hash][extname]".to_string().into()),
sanitize_filename: raw_options.sanitize_filename.unwrap_or_default(),
banner: raw_options.banner,
footer: raw_options.footer,
post_banner: raw_options.post_banner,
post_footer: raw_options.post_footer,
intro: raw_options.intro,
outro: raw_options.outro,
es_module: raw_options.es_module.unwrap_or_default(),
dir: raw_options.dir,
out_dir,
file: raw_options.file,
format,
exports: raw_options.exports.unwrap_or(crate::OutputExports::Auto),
hash_characters: raw_options.hash_characters.unwrap_or(crate::HashCharacters::Base64),
globals,
paths: raw_options.paths,
generated_code,
sourcemap: raw_options.sourcemap,
sourcemap_base_url: raw_options.sourcemap_base_url,
sourcemap_ignore_list: raw_options.sourcemap_ignore_list,
sourcemap_path_transform: raw_options.sourcemap_path_transform,
sourcemap_debug_ids: raw_options.sourcemap_debug_ids.unwrap_or(false),
sourcemap_exclude_sources: raw_options.sourcemap_exclude_sources.unwrap_or(false),
shim_missing_exports: raw_options.shim_missing_exports.unwrap_or(false),
module_types,
experimental,
profiler_names: raw_options.profiler_names.unwrap_or(false),
minify: MinifyOptions::Disabled,
define,
inject: raw_options.inject.unwrap_or_default(),
oxc_inject_global_variables_config,
extend: raw_options.extend.unwrap_or(false),
external_live_bindings: raw_options.external_live_bindings.unwrap_or(true),
code_splitting,
dynamic_import_in_cjs: raw_options.dynamic_import_in_cjs.unwrap_or(true),
manual_code_splitting: raw_options.manual_code_splitting,
checks: raw_options.checks.unwrap_or_default().into(),
watch: raw_options.watch.unwrap_or_default(),
legal_comments: raw_options.legal_comments.unwrap_or(LegalComments::Inline),
comments: {
let mut comments = raw_options.comments.unwrap_or_default();
if raw_options.comments.is_none() {
if let Some(legal) = raw_options.legal_comments {
comments.legal = matches!(legal, LegalComments::Inline);
}
}
comments
},
drop_labels: FxHashSet::from_iter(raw_options.drop_labels.unwrap_or_default()),
keep_names: raw_options.keep_names.unwrap_or_default(),
polyfill_require: raw_options.polyfill_require.unwrap_or(true),
defer_sync_scan_data: raw_options.defer_sync_scan_data,
transform_options,
make_absolute_externals_relative: raw_options
.make_absolute_externals_relative
.unwrap_or_default(),
invalidate_js_side_cache: raw_options.invalidate_js_side_cache,
log_level: raw_options.log_level,
on_log: raw_options.on_log,
preserve_modules: raw_options.preserve_modules.unwrap_or_default(),
virtual_dirname: raw_options
.virtual_dirname
.map(ArcStr::from)
.unwrap_or_else(|| arcstr::literal!("_virtual")),
preserve_modules_root: raw_options.preserve_modules_root.map(|preserve_modules_root| {
let p = Path::new(&preserve_modules_root);
cwd.join(p).normalize().to_string_lossy().to_string()
}),
cwd,
preserve_entry_signatures,
devtools: raw_options.devtools.is_some(),
optimization: normalize_optimization_option(raw_options.optimization, platform),
top_level_var: raw_options.top_level_var.unwrap_or(false),
minify_internal_exports: raw_options
.minify_internal_exports
.unwrap_or_else(|| determine_minify_internal_exports_default(Some(format), &raw_minify)),
clean_dir: raw_options.clean_dir.unwrap_or(false),
context: raw_options.context.unwrap_or_default(),
strict_execution_order: raw_options.strict_execution_order.unwrap_or(false),
strict: raw_options.strict.unwrap_or_default(),
};
normalized.minify = raw_minify.normalize(&normalized);
Ok(PrepareBuildContext { fs, resolver, options: Arc::new(normalized), warnings })
}