use rolldown_common::{AddonRenderContext, ExternalModule, OutputExports};
use rolldown_error::{BuildDiagnostic, BuildResult};
use rolldown_sourcemap::SourceJoiner;
use rolldown_utils::concat_string;
use crate::{
ecmascript::{
ecma_generator::RenderedModuleSources, format::utils::namespace::generate_namespace_definition,
},
types::generator::GenerateContext,
utils::chunk::{
namespace_marker::render_namespace_markers,
render_chunk_exports::{
get_chunk_export_names_with_ctx, render_chunk_exports, render_wrapped_entry_chunk,
},
},
};
use super::utils::{
namespace::render_property_access, render_chunk_directives, render_chunk_external_imports,
render_factory_parameters, render_modules_with_peek_runtime_module_at_first,
};
pub async fn render_umd<'code>(
ctx: &GenerateContext<'_>,
addon_render_context: AddonRenderContext<'code>,
module_sources: &'code RenderedModuleSources,
warnings: &mut Vec<BuildDiagnostic>,
) -> BuildResult<SourceJoiner<'code>> {
let mut source_joiner = SourceJoiner::default();
let AddonRenderContext { banner, intro, outro, footer, directives, .. } = addon_render_context;
if let Some(banner) = banner {
source_joiner.append_source(banner);
}
if !directives.is_empty() {
let rendered_chunk_directives = render_chunk_directives(directives.iter());
if !rendered_chunk_directives.is_empty() {
source_joiner.append_source(rendered_chunk_directives);
}
}
let export_mode = ctx.chunk.output_exports;
let export_names = get_chunk_export_names_with_ctx(ctx);
let has_exports = !export_names.is_empty();
let has_default_export = export_names.iter().any(|name| name.as_str() == "default");
let entry_module = ctx
.chunk
.entry_module(&ctx.link_output.module_table)
.expect("iife format only have entry chunk");
let named_exports = matches!(&export_mode, OutputExports::Named);
let (import_code, externals) = render_chunk_external_imports(ctx);
let externals: Vec<_> = externals.iter().map(super::utils::ExternalImportKind::module).collect();
let need_global = has_exports || named_exports || !externals.is_empty();
let wrapper_parameters = if need_global { "global, factory" } else { "factory" };
let amd_dependencies = render_amd_dependencies(ctx, &externals, has_exports && named_exports);
let global_argument = if need_global { "this, " } else { "" };
let factory_parameters = render_factory_parameters(ctx, &externals, has_exports && named_exports);
let cjs_intro = if need_global {
let cjs_export = if has_exports && !named_exports { "module.exports = " } else { "" };
let cjs_dependencies = render_cjs_dependencies(ctx, &externals, has_exports && named_exports);
format!(
"typeof exports === 'object' && typeof module !== 'undefined' ? {cjs_export} factory({cjs_dependencies}) :",
)
} else {
String::new()
};
let iife_start = if need_global {
"(global = typeof globalThis !== 'undefined' ? globalThis : global || self, "
} else {
""
};
let iife_end = if need_global { ")" } else { "" };
let iife_export =
render_iife_export(warnings, ctx, &externals, has_exports, named_exports).await?;
source_joiner.append_source(format!(
"(function({wrapper_parameters}) {{
{cjs_intro}
typeof define === 'function' && define.amd ? define([{amd_dependencies}], factory) :
{iife_start}{iife_export}{iife_end};
}})({global_argument}function({factory_parameters}) {{",
));
if let Some(intro) = intro {
source_joiner.append_source(intro);
}
if named_exports && entry_module.exports_kind.is_esm() {
if let Some(marker) = render_namespace_markers(
ctx.options.es_module,
has_default_export,
&ctx.options.generated_code,
ctx.chunk.is_entry_point(),
) {
source_joiner.append_source(marker.to_string());
}
}
render_modules_with_peek_runtime_module_at_first(
ctx,
&mut source_joiner,
module_sources,
import_code,
);
if let Some(source) = render_wrapped_entry_chunk(ctx, Some(&export_mode)) {
source_joiner.append_source(source);
}
if let Some(exports) = render_chunk_exports(ctx, Some(&export_mode)) {
source_joiner.append_source(exports);
}
if let Some(outro) = outro {
source_joiner.append_source(outro);
}
source_joiner.append_source("});");
if let Some(footer) = footer {
source_joiner.append_source(footer);
}
Ok(source_joiner)
}
fn render_amd_dependencies(
ctx: &GenerateContext<'_>,
externals: &[&ExternalModule],
has_exports: bool,
) -> String {
let mut dependencies = Vec::with_capacity(externals.len());
if has_exports {
dependencies.reserve(1);
dependencies.push("'exports'".to_string());
}
externals.iter().for_each(|external| {
dependencies.push(concat_string!(
"'",
external.get_import_path(ctx.chunk, ctx.resolved_paths),
"'"
));
});
dependencies.join(", ")
}
fn render_cjs_dependencies(
ctx: &GenerateContext<'_>,
externals: &[&ExternalModule],
has_exports: bool,
) -> String {
let mut dependencies = Vec::with_capacity(externals.len());
if has_exports {
dependencies.reserve(1);
dependencies.push("exports".to_string());
}
externals.iter().for_each(|external| {
dependencies.push(concat_string!(
"require('",
external.get_import_path(ctx.chunk, ctx.resolved_paths),
"')"
));
});
dependencies.join(", ")
}
async fn render_iife_export(
warnings: &mut Vec<BuildDiagnostic>,
ctx: &GenerateContext<'_>,
externals: &[&ExternalModule],
has_exports: bool,
named_exports: bool,
) -> BuildResult<String> {
if has_exports && ctx.options.name.as_ref().is_none_or(String::is_empty) {
return Err(vec![BuildDiagnostic::missing_name_option_for_iife_export(true)].into());
}
let mut dependencies = Vec::with_capacity(externals.len());
for external in externals {
let global = ctx.options.globals.call(external.id.as_str()).await;
let target = match &global {
Some(global_name) => global_name.split('.').map(render_property_access).collect::<String>(),
None => {
warnings.push(
BuildDiagnostic::missing_global_name(
external.id.to_string(),
external.name.clone(),
external.identifier_name.clone(),
)
.with_severity_warning(),
);
render_property_access(&external.identifier_name)
}
};
dependencies.push(format!("global{target}"));
}
let deps = dependencies.join(",");
if has_exports {
let (stmt, namespace) = generate_namespace_definition(
ctx.options.name.as_ref().expect("should have name"),
"global",
",",
);
if named_exports {
Ok(format!(
"factory(({stmt}{namespace} = {}){})",
if ctx.options.extend { format!("{namespace} || {{}}") } else { "{}".to_string() },
if dependencies.is_empty() { String::new() } else { format!(", {deps}") }
))
} else {
Ok(format!("({stmt}{namespace} = factory({deps}))"))
}
} else {
Ok(format!("factory({deps})"))
}
}