1use crate::bundle::bundle_handle::BundleHandle;
2
3use super::super::{
4 SharedOptions, SharedResolver,
5 module_loader::deferred_scan_data::defer_sync_scan_data,
6 stages::{
7 generate_stage::GenerateStage,
8 link_stage::LinkStage,
9 scan_stage::{NormalizedScanStageOutput, ScanStage, ScanStageOutput},
10 },
11 types::{bundle_output::BundleOutput, scan_stage_cache::ScanStageCache},
12 utils::fs_utils::clean_dir,
13};
14use anyhow::Context;
15use arcstr::ArcStr;
16use rolldown_common::{GetLocalDbMut, Module, ScanMode, SharedFileEmitter, SymbolRefDb};
17use rolldown_devtools::{action, trace_action, trace_action_enabled};
18use rolldown_error::{BuildDiagnostic, BuildResult, Severity};
19use rolldown_fs::{FileSystem, OsFileSystem};
20use rolldown_plugin::{
21 HookBuildEndArgs, HookCloseBundleArgs, HookRenderErrorArgs, SharedPluginDriver,
22};
23use rolldown_utils::dashmap::FxDashSet;
24use std::{path::Path, sync::Arc};
25use sugar_path::SugarPath;
26
27#[expect(
28 clippy::struct_field_names,
29 reason = "`bundle_span` emphasizes this's a span for this bundle, not a session level span"
30)]
31pub struct Bundle<Fs: FileSystem + Clone + 'static = OsFileSystem> {
32 pub(crate) fs: Fs,
33 pub(crate) options: SharedOptions,
34 pub(crate) resolver: SharedResolver<Fs>,
35 pub(crate) file_emitter: SharedFileEmitter,
36 pub(crate) plugin_driver: SharedPluginDriver,
37 pub(crate) warnings: Vec<BuildDiagnostic>,
38 pub(crate) cache: ScanStageCache,
39 pub(crate) bundle_span: Arc<tracing::Span>,
40}
41
42impl<Fs: FileSystem + Clone + 'static> Bundle<Fs> {
43 #[tracing::instrument(level = "debug", skip_all, parent = &*self.bundle_span)]
44 pub async fn write(mut self) -> BuildResult<BundleOutput> {
46 let start = self.plugin_driver.start_timing();
47 let result = async {
48 self.trace_action_session_meta();
49 trace_action!(action::BuildStart { action: "BuildStart" });
50 let scan_stage_output = self.scan_modules(ScanMode::Full).await?;
51
52 let ret = self.bundle_write(scan_stage_output).await;
53 trace_action!(action::BuildEnd { action: "BuildEnd" });
54 ret
55 }
56 .await;
57 self.plugin_driver.set_total_build_time(start);
58 self.append_plugin_timings_warning(result)
59 }
60
61 #[tracing::instrument(level = "debug", skip_all, parent = &*self.bundle_span)]
62 pub async fn generate(mut self) -> BuildResult<BundleOutput> {
64 let start = self.plugin_driver.start_timing();
65 let result = async {
66 self.trace_action_session_meta();
67 trace_action!(action::BuildStart { action: "BuildStart" });
68 let scan_stage_output = self.scan_modules(ScanMode::Full).await?;
69
70 let ret = self.bundle_generate(scan_stage_output).await.map(|mut output| {
71 output.warnings.append(&mut self.warnings);
72 output
73 });
74 trace_action!(action::BuildEnd { action: "BuildEnd" });
75 ret
76 }
77 .await;
78 self.plugin_driver.set_total_build_time(start);
79 self.append_plugin_timings_warning(result)
80 }
81
82 #[tracing::instrument(level = "debug", skip_all, parent = &*self.bundle_span)]
83 pub async fn scan(mut self) -> BuildResult<()> {
85 self.scan_modules(ScanMode::Full).await?;
86
87 Ok(())
88 }
89
90 #[tracing::instrument(level = "debug", skip_all, parent = &*self.bundle_span)]
91 pub async fn scan_modules(
92 &mut self,
93 scan_mode: ScanMode<ArcStr>,
94 ) -> BuildResult<NormalizedScanStageOutput> {
95 trace_action!(action::BuildStart { action: "BuildStart" });
96 let is_full_scan_mode = scan_mode.is_full();
97
98 let scan_stage_output = match ScanStage::new(
99 Arc::clone(&self.options),
100 Arc::clone(&self.plugin_driver),
101 self.fs.clone(),
102 Arc::clone(&self.resolver),
103 )
104 .scan(scan_mode, &mut self.cache)
105 .await
106 {
107 Ok(v) => v,
108 Err(errs) => {
109 debug_assert!(errs.iter().all(|e| e.severity() == Severity::Error));
110 self
111 .plugin_driver
112 .build_end(Some(&HookBuildEndArgs { errors: &errs, cwd: &self.options.cwd }))
113 .await?;
114 self
115 .plugin_driver
116 .close_bundle(Some(&HookCloseBundleArgs { errors: &errs, cwd: &self.options.cwd }))
117 .await?;
118 return Err(errs);
119 }
120 };
121
122 let scan_stage_output = self
123 .normalize_scan_stage_output_and_update_cache(scan_stage_output, is_full_scan_mode)
124 .await?;
125
126 if !self.options.experimental.is_incremental_build_enabled() {
128 std::mem::take(&mut self.cache);
129 }
130
131 Self::trace_action_module_graph_ready(&scan_stage_output);
132 self.plugin_driver.build_end(None).await?;
133 trace_action!(action::BuildEnd { action: "BuildEnd" });
134 Ok(scan_stage_output)
135 }
136
137 #[inline]
138 pub fn options(&self) -> &SharedOptions {
139 &self.options
140 }
141
142 pub fn get_watch_files(&self) -> &Arc<FxDashSet<ArcStr>> {
143 &self.plugin_driver.watch_files
144 }
145
146 pub fn context(&self) -> BundleHandle {
147 BundleHandle {
148 options: Arc::clone(&self.options),
149 plugin_driver: Arc::clone(&self.plugin_driver),
150 closed: Arc::default(),
151 }
152 }
153
154 #[tracing::instrument(level = "debug", skip_all, parent = &*self.bundle_span)]
155 pub async fn bundle_write(
156 &mut self,
157 scan_stage_output: NormalizedScanStageOutput,
158 ) -> BuildResult<BundleOutput> {
159 let dist_dir = self.options.cwd.join(&self.options.out_dir);
160
161 if self.options.clean_dir && self.options.file.is_none() {
162 if let Err(err) = clean_dir(&self.fs, &dist_dir) {
163 self.warnings.push(
164 BuildDiagnostic::could_not_clean_directory(
165 dist_dir.display().to_string(),
166 err.to_string(),
167 )
168 .with_severity_warning(),
169 );
170 }
171 }
172
173 let mut output = self.bundle_up(scan_stage_output, true).await?;
174
175 self.fs.create_dir_all(&dist_dir).with_context(|| {
176 format!("Could not create directory for output chunks: {}", dist_dir.display())
177 })?;
178
179 for chunk in &output.assets {
180 let filename = chunk.filename();
181 if filename.contains('\0') {
182 let pattern_name = match chunk {
183 rolldown_common::Output::Chunk(c) => {
184 if c.is_entry {
185 "entryFileNames"
186 } else {
187 "chunkFileNames"
188 }
189 }
190 rolldown_common::Output::Asset(_) => "assetFileNames",
191 };
192 return Err(
193 BuildDiagnostic::invalid_option(rolldown_error::InvalidOptionType::NulByteInFilename {
194 pattern_name: pattern_name.to_string(),
195 })
196 .into(),
197 );
198 }
199 let dest = dist_dir.join(filename);
200 if let Some(p) = dest.parent() {
201 if !self.fs.exists(p) {
202 self.fs.create_dir_all(p).with_context(|| {
203 format!("Could not create directory for output chunks: {}", p.display())
204 })?;
205 }
206 }
207 self
208 .fs
209 .write(&dest, chunk.content_as_bytes())
210 .with_context(|| format!("Failed to write file in {}", dest.display()))?;
211 }
212
213 self
214 .plugin_driver
215 .write_bundle(&mut output.assets, &self.options, &mut output.warnings)
216 .await?;
217
218 output.warnings.append(&mut self.warnings);
219
220 Ok(output)
221 }
222
223 #[tracing::instrument(level = "debug", skip_all, parent = &*self.bundle_span)]
224 pub async fn bundle_generate(
225 &mut self,
226 scan_stage_output: NormalizedScanStageOutput,
227 ) -> BuildResult<BundleOutput> {
228 self.bundle_up(scan_stage_output, false).await
229 }
230
231 #[tracing::instrument(level = "debug", skip(self, output))]
232 async fn normalize_scan_stage_output_and_update_cache(
233 &mut self,
234 output: ScanStageOutput,
235 is_full_scan_mode: bool,
236 ) -> BuildResult<NormalizedScanStageOutput> {
237 let is_incremental = self.options.experimental.is_incremental_build_enabled();
238
239 if is_full_scan_mode {
240 let mut output: NormalizedScanStageOutput =
241 output.try_into().expect("Should be able to convert to NormalizedScanStageOutput");
242 let result =
248 defer_sync_scan_data(&self.options, &self.cache.module_id_to_idx, &mut output).await;
249 if is_incremental {
250 self.cache.set_snapshot(output.make_copy());
251 }
252 result?;
253 return Ok(output);
254 }
255
256 self.cache.merge(output)?;
257 self.cache.update_defer_sync_data(&self.options).await?;
258 Ok(self.cache.create_output())
259 }
260
261 async fn bundle_up(
262 &mut self,
263 scan_stage_output: NormalizedScanStageOutput,
264 is_write: bool,
265 ) -> BuildResult<BundleOutput> {
266 let start = self.plugin_driver.start_timing();
267 let (mut link_stage_output, ast_table) =
268 LinkStage::new(scan_stage_output, &self.options).link();
269 self.plugin_driver.set_link_stage_time(start);
270
271 let bundle_output =
272 GenerateStage::new(&mut link_stage_output, ast_table, &self.options, &self.plugin_driver)
273 .generate()
274 .await; self.merge_immutable_fields_for_cache(link_stage_output.symbol_db);
281
282 if let Err(errors) = &bundle_output {
283 debug_assert!(errors.iter().all(|e| e.severity() == Severity::Error));
284 self
285 .plugin_driver
286 .render_error(&HookRenderErrorArgs { errors, cwd: &self.options.cwd })
287 .await?;
288 }
289
290 let mut output = bundle_output?;
291
292 self.file_emitter.add_additional_files(&mut output.assets, &mut output.warnings);
294
295 self
296 .plugin_driver
297 .generate_bundle(&mut output.assets, is_write, &self.options, &mut output.warnings)
298 .await?;
299
300 for asset in &output.assets {
301 if is_filename_outside_output_dir(asset.filename()) {
302 return Err(
303 vec![BuildDiagnostic::filename_outside_output_directory(asset.filename().to_string())]
304 .into(),
305 );
306 }
307 }
308
309 if let Some(invalidate_js_side_cache) = &self.options.invalidate_js_side_cache {
310 invalidate_js_side_cache.call().await?;
311 }
312
313 Ok(output)
314 }
315
316 fn merge_immutable_fields_for_cache(&mut self, symbol_db: SymbolRefDb) {
317 if !self.options.experimental.is_incremental_build_enabled() {
318 return;
319 }
320 let snapshot = self.cache.get_snapshot_mut();
321 for (idx, symbol_ref_db) in symbol_db.into_inner().into_iter_enumerated() {
322 let Some(db_for_module) = symbol_ref_db else {
323 continue;
324 };
325 let cache_db = snapshot.symbol_ref_db.local_db_mut(idx);
326 cache_db.merge_from_build(db_for_module);
327 }
328 }
329
330 fn trace_action_module_graph_ready(scan_stage_output: &NormalizedScanStageOutput) {
331 if trace_action_enabled!() {
332 let modules = scan_stage_output
333 .module_table
334 .modules
335 .iter()
336 .map(|m| match m {
337 Module::Normal(module) => action::Module {
338 id: module.id.to_string(),
339 is_external: false,
340 imports: Some(
341 module
342 .import_records
343 .iter()
344 .filter_map(|r| {
345 r.resolved_module.map(|module_idx| action::ModuleImport {
346 module_id: scan_stage_output.module_table[module_idx].id().to_string(),
347 kind: r.kind.to_string(),
348 module_request: r.module_request.to_string(),
349 })
350 })
351 .collect(),
352 ),
353 importers: Some(module.importers.iter().map(ToString::to_string).collect()),
354 },
355 Module::External(module) => action::Module {
356 id: module.id.to_string(),
357 is_external: true,
358 imports: None,
359 importers: None,
360 },
361 })
362 .collect();
363 trace_action!(action::ModuleGraphReady { action: "ModuleGraphReady", modules });
364 }
365 }
366
367 fn trace_action_session_meta(&self) {
368 if trace_action_enabled!() {
369 trace_action!(action::SessionMeta {
370 action: "SessionMeta",
371 inputs: self
372 .options
373 .input
374 .iter()
375 .map(|v| action::InputItem { name: v.name.clone(), filename: v.import.clone() })
376 .collect(),
377 plugins: self
378 .plugin_driver
379 .plugins()
380 .iter()
381 .enumerate()
382 .map(|(idx, p)| action::PluginItem {
383 name: p.call_name().into_owned(),
384 plugin_id: idx.try_into().unwrap()
385 })
386 .collect(),
387 cwd: self.options.cwd.to_string_lossy().to_string(),
388 platform: self.options.platform.to_string(),
389 format: self.options.format.to_string(),
390 dir: self.options.dir.clone(),
391 file: self.options.file.clone(),
392 });
393 }
394 }
395
396 fn append_plugin_timings_warning(
398 &self,
399 result: BuildResult<BundleOutput>,
400 ) -> BuildResult<BundleOutput> {
401 result.map(|mut output| {
402 if let Some(plugins) = self.plugin_driver.get_plugin_timings_info() {
403 output.warnings.push(BuildDiagnostic::plugin_timings(plugins).with_severity_warning());
404 }
405 output
406 })
407 }
408}
409
410fn is_filename_outside_output_dir(filename: &str) -> bool {
415 if Path::new(filename).is_absolute() {
416 return true;
417 }
418
419 let normalized = filename.normalize();
420 let normalized = normalized.to_string_lossy();
421
422 normalized == "."
423 || normalized == ".."
424 || normalized.starts_with("../")
425 || normalized.starts_with("..\\")
426}