Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a6edd11

Browse files
authored
feat: format umd (#2489)
<!-- Thank you for contributing! --> ### Description <!-- Please insert your description here and provide especially info about the "what" this PR is solving -->
1 parent 450b502 commit a6edd11

86 files changed

Lines changed: 909 additions & 36 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/rolldown/src/ecmascript/ecma_generator.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ use rolldown_utils::rayon::{IntoParallelRefIterator, ParallelIterator};
1717
use rustc_hash::FxHashMap;
1818
use sugar_path::SugarPath;
1919

20-
use super::format::{app::render_app, cjs::render_cjs, esm::render_esm, iife::render_iife};
20+
use super::format::{
21+
app::render_app, cjs::render_cjs, esm::render_esm, iife::render_iife, umd::render_umd,
22+
};
2123

2224
pub type RenderedModuleSources = Vec<(ModuleIdx, ModuleId, Option<Vec<Box<dyn Source + Send>>>)>;
2325

@@ -119,6 +121,12 @@ impl Generator for EcmaGenerator {
119121
Err(errors) => return Ok(Err(errors)),
120122
}
121123
}
124+
OutputFormat::Umd => {
125+
match render_umd(ctx, rendered_module_sources, banner, footer, intro, outro) {
126+
Ok(concat_source) => concat_source,
127+
Err(errors) => return Ok(Err(errors)),
128+
}
129+
}
122130
};
123131

124132
let (content, mut map) = concat_source.content_and_sourcemap();

crates/rolldown/src/ecmascript/format/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ pub mod app;
22
pub mod cjs;
33
pub mod esm;
44
pub mod iife;
5+
pub mod umd;
56
pub mod utils;
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
use arcstr::ArcStr;
2+
use rolldown_common::{ChunkKind, OutputExports};
3+
use rolldown_error::{BuildDiagnostic, DiagnosableResult};
4+
use rolldown_sourcemap::{ConcatSource, RawSource};
5+
use rolldown_utils::ecma_script::legitimize_identifier_name;
6+
7+
use crate::{
8+
ecmascript::{
9+
ecma_generator::RenderedModuleSources, format::utils::namespace::generate_namespace_definition,
10+
},
11+
types::generator::GenerateContext,
12+
utils::chunk::{
13+
collect_render_chunk_imports::ExternalRenderImportStmt,
14+
determine_export_mode::determine_export_mode,
15+
determine_use_strict::determine_use_strict,
16+
namespace_marker::render_namespace_markers,
17+
render_chunk_exports::{get_export_items, render_chunk_exports},
18+
},
19+
};
20+
21+
use super::utils::{
22+
namespace::render_property_access, render_chunk_external_imports, render_factory_parameters,
23+
};
24+
25+
pub fn render_umd(
26+
ctx: &mut GenerateContext<'_>,
27+
module_sources: RenderedModuleSources,
28+
banner: Option<String>,
29+
footer: Option<String>,
30+
intro: Option<String>,
31+
outro: Option<String>,
32+
) -> DiagnosableResult<ConcatSource> {
33+
let mut concat_source = ConcatSource::default();
34+
35+
if let Some(banner) = banner {
36+
concat_source.add_source(Box::new(RawSource::new(banner)));
37+
}
38+
39+
// umd wrapper start
40+
41+
// Analyze the export information of the chunk.
42+
let export_items = get_export_items(ctx.chunk, ctx.link_output);
43+
let has_exports = !export_items.is_empty();
44+
let has_default_export = export_items.iter().any(|(name, _)| name.as_str() == "default");
45+
46+
let entry_module = match ctx.chunk.kind {
47+
ChunkKind::EntryPoint { module, .. } => {
48+
&ctx.link_output.module_table.modules[module].as_normal().expect("should be normal module")
49+
}
50+
ChunkKind::Common => unreachable!("umd should be entry point chunk"),
51+
};
52+
53+
// We need to transform the `OutputExports::Auto` to suitable `OutputExports`.
54+
let export_mode = determine_export_mode(ctx, entry_module, &export_items)?;
55+
56+
let named_exports = matches!(&export_mode, OutputExports::Named);
57+
58+
// It is similar to CJS.
59+
let (import_code, externals) = render_chunk_external_imports(ctx);
60+
61+
// The function argument and the external imports are passed as arguments to the wrapper function.
62+
let need_global = has_exports || named_exports || !externals.is_empty();
63+
let wrapper_parameters = if need_global { "global, factory" } else { "factory" };
64+
let amd_dependencies = render_amd_dependencies(&externals, has_exports && named_exports);
65+
let global_argument = if need_global { "this, " } else { "" };
66+
let factory_parameters = render_factory_parameters(ctx, &externals, has_exports && named_exports);
67+
let cjs_intro = if need_global {
68+
let cjs_export = if has_exports && !named_exports { "module.exports = " } else { "" };
69+
let cjs_dependencies = render_cjs_dependencies(&externals, has_exports && named_exports);
70+
format!("typeof exports === 'object' && typeof module !== 'undefined' ? {cjs_export} factory({cjs_dependencies}) :",)
71+
} else {
72+
String::new()
73+
};
74+
let iife_start = if need_global {
75+
"(global = typeof globalThis !== 'undefined' ? globalThis : global || self, "
76+
} else {
77+
""
78+
};
79+
let iife_end = if need_global { ")" } else { "" };
80+
let iife_export = render_iife_export(ctx, &externals, has_exports, named_exports)?;
81+
concat_source.add_source(Box::new(RawSource::new(format!(
82+
"(function({wrapper_parameters}) {{
83+
{cjs_intro}
84+
typeof define === 'function' && define.amd ? define([{amd_dependencies}], factory) :
85+
{iife_start}{iife_export}{iife_end};
86+
}})({global_argument}function({factory_parameters}) {{",
87+
))));
88+
89+
if determine_use_strict(ctx) {
90+
concat_source.add_source(Box::new(RawSource::new("\"use strict\";".to_string())));
91+
}
92+
93+
if let Some(intro) = intro {
94+
concat_source.add_source(Box::new(RawSource::new(intro)));
95+
}
96+
97+
if named_exports {
98+
if let Some(marker) =
99+
render_namespace_markers(&ctx.options.es_module, has_default_export, false)
100+
{
101+
concat_source.add_source(Box::new(RawSource::new(marker.into())));
102+
}
103+
}
104+
105+
concat_source.add_source(Box::new(RawSource::new(import_code)));
106+
107+
// chunk content
108+
// TODO indent chunk content
109+
module_sources.into_iter().for_each(|(_, _, module_render_output)| {
110+
if let Some(emitted_sources) = module_render_output {
111+
for source in emitted_sources {
112+
concat_source.add_source(source);
113+
}
114+
}
115+
});
116+
117+
// exports
118+
if let Some(exports) = render_chunk_exports(ctx, Some(&export_mode)) {
119+
concat_source.add_source(Box::new(RawSource::new(exports)));
120+
}
121+
122+
if let Some(outro) = outro {
123+
concat_source.add_source(Box::new(RawSource::new(outro)));
124+
}
125+
126+
// umd wrapper end
127+
concat_source.add_source(Box::new(RawSource::new("});".to_string())));
128+
129+
if let Some(footer) = footer {
130+
concat_source.add_source(Box::new(RawSource::new(footer)));
131+
}
132+
133+
Ok(concat_source)
134+
}
135+
136+
fn render_amd_dependencies(externals: &[ExternalRenderImportStmt], has_exports: bool) -> String {
137+
let mut dependencies = Vec::with_capacity(externals.len());
138+
if has_exports {
139+
dependencies.reserve(1);
140+
dependencies.push("exports".to_string());
141+
}
142+
externals.iter().for_each(|external| {
143+
dependencies.push(format!("'{}'", external.path.as_str()));
144+
});
145+
dependencies.join(", ")
146+
}
147+
148+
fn render_cjs_dependencies(externals: &[ExternalRenderImportStmt], has_exports: bool) -> String {
149+
let mut dependencies = Vec::with_capacity(externals.len());
150+
if has_exports {
151+
dependencies.reserve(1);
152+
dependencies.push("exports".to_string());
153+
}
154+
externals.iter().for_each(|external| {
155+
dependencies.push(format!("require('{}')", external.path.as_str()));
156+
});
157+
dependencies.join(", ")
158+
}
159+
160+
fn render_iife_export(
161+
ctx: &mut GenerateContext<'_>,
162+
externals: &[ExternalRenderImportStmt],
163+
has_exports: bool,
164+
named_exports: bool,
165+
) -> DiagnosableResult<String> {
166+
if ctx.options.name.as_ref().map_or(true, String::is_empty) {
167+
return Err(vec![BuildDiagnostic::missing_name_option_for_umd_export()]);
168+
}
169+
let (stmt, namespace) = generate_namespace_definition(
170+
ctx.options.name.as_ref().expect("should have name"),
171+
"global",
172+
",",
173+
);
174+
let mut dependencies = Vec::with_capacity(externals.len());
175+
externals.iter().for_each(|external| {
176+
if let Some(global) = ctx.options.globals.get(external.path.as_str()) {
177+
dependencies.push(format!(
178+
"global{}",
179+
global.split('.').map(render_property_access).collect::<String>()
180+
));
181+
} else {
182+
let target = legitimize_identifier_name(external.path.as_str()).to_string();
183+
ctx.warnings.push(
184+
BuildDiagnostic::missing_global_name(external.path.clone(), ArcStr::from(&target))
185+
.with_severity_warning(),
186+
);
187+
dependencies.push(format!("global{}", render_property_access(&target)));
188+
}
189+
});
190+
let deps = dependencies.join(",");
191+
if has_exports {
192+
if named_exports {
193+
Ok(format!(
194+
"factory(({stmt}{namespace} = {}){})",
195+
if ctx.options.extend { format!("{namespace} || {{}}") } else { "{}".to_string() },
196+
if dependencies.is_empty() { String::new() } else { format!(", {deps}") }
197+
))
198+
} else {
199+
Ok(format!("({stmt}{namespace} = factory({deps}))"))
200+
}
201+
} else {
202+
Ok(format!("factory({deps})"))
203+
}
204+
}

crates/rolldown/src/ecmascript/format/utils/namespace.rs

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,24 +26,27 @@ use rolldown_utils::ecma_script::is_validate_assignee_identifier_name;
2626
/// ```js
2727
/// this.namespace.module.hello
2828
/// ```
29-
fn generate_namespace_definition(name: &str) -> (String, String) {
30-
let mut initial_code = String::new();
31-
let mut final_code = String::from("this");
32-
33-
let context_len = final_code.len();
29+
pub fn generate_namespace_definition(
30+
name: &str,
31+
global: &str,
32+
delimiter: &str,
33+
) -> (String, String) {
3434
let parts: Vec<&str> = name.split('.').collect();
35+
let mut stmts = String::new();
36+
let mut namespace = String::from(global);
37+
let global_len = global.len();
3538

3639
for (i, part) in parts.iter().enumerate() {
37-
let caller = generate_caller(part);
38-
final_code.push_str(&caller);
40+
let property = render_property_access(part);
41+
namespace.push_str(&property);
3942

4043
if i < parts.len() - 1 {
41-
let callers = &final_code[context_len..];
42-
initial_code.push_str(&format!("this{callers} = this{callers} || {{}};\n"));
44+
let property = &namespace[global_len..];
45+
stmts.push_str(&format!("{global}{property} = {global}{property} || {{}}{delimiter}"));
4346
}
4447
}
4548

46-
(initial_code, final_code)
49+
(stmts, namespace)
4750
}
4851

4952
/// This function generates a namespace definition for the given name, especially for IIFE format or UMD format.
@@ -80,31 +83,31 @@ pub fn generate_identifier(
8083

8184
// It is same as Rollup.
8285
if name.contains('.') {
83-
let (decl, expr) = generate_namespace_definition(name);
86+
let (stmts, namespace) = generate_namespace_definition(name, "this", ";\n");
8487
// Extend the object if the `extend` option is enabled.
8588
let final_expr = if ctx.options.extend && matches!(export_mode, OutputExports::Named) {
86-
format!("{expr} = {expr} || {{}}")
89+
format!("{namespace} = {namespace} || {{}}")
8790
} else {
88-
expr
91+
namespace
8992
};
9093

91-
return Ok((decl, final_expr));
94+
return Ok((stmts, final_expr));
9295
}
9396

9497
if ctx.options.extend {
95-
let caller = generate_caller(name.as_str());
98+
let property = render_property_access(name.as_str());
9699
let final_expr = if matches!(export_mode, OutputExports::Named) {
97100
// In named exports, the `extend` option will make the assignment disappear and
98101
// the modification will be done extending the existed object (the `name` option).
99-
format!("this{caller} = this{caller} || {{}}")
102+
format!("this{property} = this{property} || {{}}")
100103
} else {
101104
// If there isn't a name in default export, we shouldn't assign the function to `this[""]`.
102105
// If there is, we should assign the function to `this["name"]`,
103106
// because there isn't an object that we can extend.
104107
if name.is_empty() {
105108
String::new()
106109
} else {
107-
format!("this{caller}")
110+
format!("this{property}")
108111
}
109112
};
110113

@@ -125,7 +128,7 @@ pub fn generate_identifier(
125128
///
126129
/// - If the name is not a reserved word and not an invalid identifier, it will generate a caller like `.name`.
127130
/// - Otherwise, it will generate a caller like `["if"]`.
128-
fn generate_caller(name: &str) -> String {
131+
pub fn render_property_access(name: &str) -> String {
129132
if is_validate_assignee_identifier_name(name) {
130133
format!(".{name}")
131134
} else {
@@ -139,14 +142,14 @@ mod tests {
139142

140143
#[test]
141144
fn test_generate_namespace_definition() {
142-
let result = generate_namespace_definition("a.b.c");
145+
let result = generate_namespace_definition("a.b.c", "this", ";\n");
143146
assert_eq!(result.0, "this.a = this.a || {};\nthis.a.b = this.a.b || {};\n");
144147
assert_eq!(result.1, "this.a.b.c");
145148
}
146149

147150
#[test]
148151
fn test_reserved_identifier_as_name() {
149-
let result = generate_namespace_definition("1.2.3");
152+
let result = generate_namespace_definition("1.2.3", "this", ";\n");
150153
assert_eq!(
151154
result.0,
152155
"this[\"1\"] = this[\"1\"] || {};\nthis[\"1\"][\"2\"] = this[\"1\"][\"2\"] || {};\n"
@@ -157,7 +160,7 @@ mod tests {
157160
#[test]
158161
/// It is related a bug in rollup. Check it out in [rollup/rollup#5603](https://github.com/rollup/rollup/issues/5603).
159162
fn test_invalid_identifier_as_name() {
160-
let result = generate_namespace_definition("toString.valueOf.constructor");
163+
let result = generate_namespace_definition("toString.valueOf.constructor", "this", ";\n");
161164
assert_eq!(result.0, "this.toString = this.toString || {};\nthis.toString.valueOf = this.toString.valueOf || {};\n");
162165
assert_eq!(result.1, "this.toString.valueOf.constructor");
163166
}

crates/rolldown/src/module_finalizers/scope_hoisting/impl_visit_mut.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ impl<'me, 'ast> VisitMut<'ast> for ScopeHoistingFinalizer<'me, 'ast> {
110110
match self.ctx.options.format {
111111
rolldown_common::OutputFormat::Esm
112112
| rolldown_common::OutputFormat::Iife
113+
| rolldown_common::OutputFormat::Umd
113114
| rolldown_common::OutputFormat::Cjs => {
114115
// Just remove the statement
115116
return;

crates/rolldown/src/module_finalizers/scope_hoisting/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ impl<'me, 'ast> ScopeHoistingFinalizer<'me, 'ast> {
229229
});
230230
re_export_external_stmts = Some(stmts.collect::<Vec<_>>());
231231
}
232-
OutputFormat::Cjs | OutputFormat::Iife => {
232+
OutputFormat::Cjs | OutputFormat::Iife | OutputFormat::Umd => {
233233
let stmts = export_all_externals_rec_ids.iter().copied().map(|idx| {
234234
// Insert `__reExport(exports, require('ext'))`
235235
let importer_namespace_name =

crates/rolldown/src/stages/generate_stage/code_splitting.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ pub type IndexSplittingInfo = IndexVec<ModuleIdx, SplittingInfo>;
2222
impl<'a> GenerateStage<'a> {
2323
#[tracing::instrument(level = "debug", skip_all)]
2424
pub async fn generate_chunks(&mut self) -> anyhow::Result<ChunkGraph> {
25-
if matches!(self.options.format, OutputFormat::Iife) {
25+
if matches!(self.options.format, OutputFormat::Iife | OutputFormat::Umd) {
2626
let user_defined_entry_count =
2727
self.link_output.entries.iter().filter(|entry| entry.kind.is_user_defined()).count();
28-
debug_assert!(user_defined_entry_count == 1, "IIFE format only supports one entry point");
28+
debug_assert!(user_defined_entry_count == 1, "IIFE/UMD format only supports one entry point");
2929
}
3030
let entries_len: u32 =
3131
self.link_output.entries.len().try_into().expect("Too many entries, u32 overflowed.");
@@ -109,7 +109,9 @@ impl<'a> GenerateStage<'a> {
109109
}
110110
}
111111

112-
if matches!(self.options.format, OutputFormat::Iife) && chunk_graph.chunk_table.len() > 1 {
112+
if matches!(self.options.format, OutputFormat::Iife | OutputFormat::Umd)
113+
&& chunk_graph.chunk_table.len() > 1
114+
{
113115
self.link_output.errors.push(BuildDiagnostic::invalid_option(
114116
InvalidOptionTypes::UnsupportedCodeSplittingFormat,
115117
self.options.format.to_string(),

crates/rolldown/src/stages/link_stage/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ impl<'a> LinkStage<'a> {
484484
declared_symbols.push(ecma_module.import_records[rec_idx].namespace_ref);
485485
});
486486
}
487-
OutputFormat::Cjs | OutputFormat::Iife | OutputFormat::App => {}
487+
OutputFormat::Cjs | OutputFormat::Iife | OutputFormat::Umd | OutputFormat::App => {}
488488
}
489489
};
490490
// Create a StmtInfo to represent the statement that declares and constructs the Module Namespace Object.

0 commit comments

Comments
 (0)