use std::{borrow::Cow, path::Path, sync::Arc};
use oxc::transformer_plugins::InjectGlobalVariablesConfig;
use rolldown_common::{
AttachDebugInfo, GlobalsOutputOption, InjectImport, LegalComments, MinifyOptions, ModuleType,
NormalizedBundlerOptions, OutputFormat, Platform, PreserveEntrySignatures, TreeshakeOptions,
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 crate::{
SharedResolver, utils::normalize_transform_options::normalize_transform_options_with_tsconfig,
};
pub struct PrepareBuildContext {
pub fs: OsFileSystem,
pub resolver: SharedResolver,
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.to_string()))
.with_severity_warning(),
);
}
}
match raw_options.format {
Some(format @ (OutputFormat::Umd | OutputFormat::Iife)) => {
if matches!(raw_options.inline_dynamic_imports, Some(false)) {
warnings.push(
BuildDiagnostic::invalid_option(InvalidOptionType::UnsupportedInlineDynamicFormat(
format.to_string(),
))
.with_severity_warning(),
);
}
}
_ => {}
}
if let Some(advanced_chunks) = &raw_options.advanced_chunks {
let has_groups = advanced_chunks.groups.as_ref().is_some_and(|groups| !groups.is_empty());
if !has_groups {
let mut specified_options = Vec::new();
if advanced_chunks.min_share_count.is_some() {
specified_options.push("minShareCount".to_string());
}
if advanced_chunks.min_size.is_some() {
specified_options.push("minSize".to_string());
}
if advanced_chunks.max_size.is_some() {
specified_options.push("maxSize".to_string());
}
if advanced_chunks.min_module_size.is_some() {
specified_options.push("minModuleSize".to_string());
}
if advanced_chunks.max_module_size.is_some() {
specified_options.push("maxModuleSize".to_string());
}
if advanced_chunks.include_dependencies_recursively.is_some() {
specified_options.push("includeDependenciesRecursively".to_string());
}
if !specified_options.is_empty() {
warnings.push(
BuildDiagnostic::invalid_option(InvalidOptionType::AdvancedChunksWithoutGroups(
specified_options,
))
.with_severity_warning(),
);
}
}
if matches!(advanced_chunks.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(advanced_chunks) = &raw_options.advanced_chunks
&& matches!(advanced_chunks.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_enabled() {
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.hmr.is_some() {
experimental.incremental_build = Some(true);
}
if experimental.attach_debug_info.is_none() {
experimental.attach_debug_info = Some(AttachDebugInfo::Simple);
}
let inline_dynamic_imports = match format {
OutputFormat::Umd | OutputFormat::Iife => true,
_ => raw_options.inline_dynamic_imports.unwrap_or(false),
};
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 tsconfig = raw_options.tsconfig.map(|tsconfig| cwd.join(tsconfig));
let mut raw_treeshake = raw_options.treeshake;
if experimental.hmr.is_some() {
raw_treeshake = TreeshakeOptions::Boolean(false);
}
let fs = OsFileSystem::new(raw_resolve.yarn_pnp.is_some_and(|b| b));
let resolver =
Arc::new(Resolver::new(fs.clone(), cwd.clone(), platform, tsconfig.clone(), raw_resolve));
let transform_options = Box::new(normalize_transform_options_with_tsconfig(
raw_options.transform.unwrap_or_default(),
tsconfig.as_ref().map(|path| resolver.resolve_tsconfig(&path)).transpose().unwrap(),
&mut warnings,
)?);
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()),
css_entry_filenames: raw_options
.css_entry_filenames
.unwrap_or_else(|| "[name].css".to_string().into()),
css_chunk_filenames: raw_options
.css_chunk_filenames
.unwrap_or_else(|| "[name]-[hash].css".to_string().into()),
sanitize_filename: raw_options.sanitize_filename.unwrap_or_default(),
banner: raw_options.banner,
footer: raw_options.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),
shim_missing_exports: raw_options.shim_missing_exports.unwrap_or(false),
module_types,
experimental,
profiler_names: raw_options.profiler_names.unwrap_or(!raw_minify.is_enabled()),
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),
inline_dynamic_imports,
advanced_chunks: raw_options.advanced_chunks,
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),
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.unwrap_or_else(|| "_virtual".to_string()),
preserve_modules_root: raw_options.preserve_modules_root.map(|preserve_modules_root| {
let p = Path::new(&preserve_modules_root);
if p.is_absolute() {
preserve_modules_root
} else {
cwd.join(p).to_string_lossy().to_string()
}
}),
cwd,
preserve_entry_signatures,
debug: raw_options.debug.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(|| {
crate::utils::determine_minify_internal_exports_default::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(),
tsconfig,
};
normalized.minify = raw_minify.normalize(&normalized);
Ok(PrepareBuildContext { fs, resolver, options: Arc::new(normalized), warnings })
}