Skip to main content

rspack_core/compiler/
mod.rs

1mod rebuild;
2use std::sync::{Arc, atomic::AtomicU32};
3
4use futures::future::join_all;
5use rspack_cacheable::cacheable;
6use rspack_error::Result;
7use rspack_fs::{IntermediateFileSystem, NativeFileSystem, ReadableFileSystem, WritableFileSystem};
8use rspack_hook::define_hook;
9use rspack_paths::{Utf8Path, Utf8PathBuf};
10use rspack_sources::BoxSource;
11use rspack_tasks::{CompilerContext, within_compiler_context};
12use rspack_util::{node_path::NodePath, tracing_preset::TRACING_BENCH_TARGET};
13use rustc_hash::FxHashMap as HashMap;
14use tracing::instrument;
15
16pub use self::rebuild::CompilationRecords;
17use crate::{
18  BoxPlugin, CleanOptions, Compilation, CompilationAsset, CompilationLogging, CompilerOptions,
19  CompilerPlatform, ContextModuleFactory, Filename, KeepPattern, NormalModuleFactory, PluginDriver,
20  ResolverFactory, SharedPluginDriver,
21  cache::{Cache, new_cache},
22  compilation::build_module_graph::ModuleExecutor,
23  fast_set, include_hash,
24  incremental::{Incremental, IncrementalPasses},
25  logger::Logger,
26  trim_dir,
27};
28
29// should be SyncHook, but rspack need call js hook
30define_hook!(CompilerThisCompilation: Series(compilation: &mut Compilation, params: &mut CompilationParams));
31// should be SyncHook, but rspack need call js hook
32define_hook!(CompilerCompilation: Series(compilation: &mut Compilation, params: &mut CompilationParams));
33// should be AsyncParallelHook
34define_hook!(CompilerMake: Series(compilation: &mut Compilation));
35define_hook!(CompilerFinishMake: Series(compilation: &mut Compilation));
36// should be SyncBailHook, but rspack need call js hook
37define_hook!(CompilerShouldEmit: SeriesBail(compilation: &mut Compilation) -> bool);
38define_hook!(CompilerShouldRecord: SeriesBail(compilation: &mut Compilation) -> bool);
39define_hook!(CompilerEmit: Series(compilation: &mut Compilation));
40define_hook!(CompilerAfterEmit: Series(compilation: &mut Compilation));
41define_hook!(CompilerAssetEmitted: Series(compilation: &Compilation, filename: &str, info: &AssetEmittedInfo));
42define_hook!(CompilerClose: Series(compilation: &Compilation));
43define_hook!(CompilerDone: Series(compilation: &Compilation));
44define_hook!(CompilerFailed: Series(compilation: &Compilation));
45
46#[derive(Debug, Default)]
47pub struct CompilerHooks {
48  pub this_compilation: CompilerThisCompilationHook,
49  pub compilation: CompilerCompilationHook,
50  pub make: CompilerMakeHook,
51  pub finish_make: CompilerFinishMakeHook,
52  pub should_emit: CompilerShouldEmitHook,
53  pub should_record: CompilerShouldRecordHook,
54  pub emit: CompilerEmitHook,
55  pub after_emit: CompilerAfterEmitHook,
56  pub asset_emitted: CompilerAssetEmittedHook,
57  pub close: CompilerCloseHook,
58  pub done: CompilerDoneHook,
59  pub failed: CompilerFailedHook,
60}
61
62static COMPILER_ID: AtomicU32 = AtomicU32::new(0);
63
64#[cacheable]
65#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Ord, PartialOrd)]
66pub struct CompilerId(u32);
67
68impl CompilerId {
69  pub fn new() -> Self {
70    Self(COMPILER_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed))
71  }
72}
73
74impl Default for CompilerId {
75  fn default() -> Self {
76    Self::new()
77  }
78}
79impl CompilerId {
80  pub fn as_u32(&self) -> u32 {
81    self.0
82  }
83}
84
85#[derive(Debug)]
86pub struct Compiler {
87  id: CompilerId,
88  pub compiler_path: String,
89  pub options: Arc<CompilerOptions>,
90  pub output_filesystem: Arc<dyn WritableFileSystem>,
91  pub intermediate_filesystem: Arc<dyn IntermediateFileSystem>,
92  pub input_filesystem: Arc<dyn ReadableFileSystem>,
93  pub compilation: Compilation,
94  pub plugin_driver: SharedPluginDriver,
95  pub buildtime_plugin_driver: SharedPluginDriver,
96  pub resolver_factory: Arc<ResolverFactory>,
97  pub loader_resolver_factory: Arc<ResolverFactory>,
98  pub cache: Box<dyn Cache>,
99  /// emitted asset versions
100  /// the key of HashMap is filename, the value of HashMap is version
101  pub emitted_asset_versions: HashMap<String, String>,
102  pub platform: Arc<CompilerPlatform>,
103  compiler_context: Arc<CompilerContext>,
104  last_records: Option<Arc<CompilationRecords>>,
105}
106
107impl Compiler {
108  #[allow(clippy::too_many_arguments)]
109  pub fn new(
110    compiler_path: String,
111    options: CompilerOptions,
112    plugins: Vec<BoxPlugin>,
113    buildtime_plugins: Vec<BoxPlugin>,
114    output_filesystem: Option<Arc<dyn WritableFileSystem>>,
115    intermediate_filesystem: Option<Arc<dyn IntermediateFileSystem>>,
116    // only supports passing input_filesystem in rust api, no support for js api
117    input_filesystem: Option<Arc<dyn ReadableFileSystem>>,
118    // no need to pass resolve_factory in rust api
119    resolver_factory: Option<Arc<ResolverFactory>>,
120    loader_resolver_factory: Option<Arc<ResolverFactory>>,
121    compiler_context: Option<Arc<CompilerContext>>,
122    platform: Arc<CompilerPlatform>,
123  ) -> Self {
124    #[cfg(debug_assertions)]
125    {
126      if let Ok(mut debug_info) = crate::debug_info::DEBUG_INFO.lock() {
127        debug_info.with_context(options.context.to_string());
128      }
129    }
130    let pnp = options.resolve.pnp.unwrap_or(false);
131    // pnp is only meaningful for input_filesystem, so disable it for intermediate_filesystem and output_filesystem
132    let input_filesystem = input_filesystem.unwrap_or_else(|| Arc::new(NativeFileSystem::new(pnp)));
133
134    let output_filesystem =
135      output_filesystem.unwrap_or_else(|| Arc::new(NativeFileSystem::new(false)));
136    let intermediate_filesystem =
137      intermediate_filesystem.unwrap_or_else(|| Arc::new(NativeFileSystem::new(false)));
138
139    let resolver_factory = resolver_factory.unwrap_or_else(|| {
140      Arc::new(ResolverFactory::new(
141        options.resolve.clone(),
142        input_filesystem.clone(),
143      ))
144    });
145    let loader_resolver_factory = loader_resolver_factory.unwrap_or_else(|| {
146      Arc::new(ResolverFactory::new(
147        options.resolve_loader.clone(),
148        input_filesystem.clone(),
149      ))
150    });
151
152    let options = Arc::new(options);
153    let compilation_logging: CompilationLogging = Default::default();
154    let plugin_driver = PluginDriver::new(options.clone(), plugins, resolver_factory.clone());
155    let buildtime_plugin_driver =
156      PluginDriver::new(options.clone(), buildtime_plugins, resolver_factory.clone());
157    let cache = new_cache(
158      &compiler_path,
159      options.clone(),
160      input_filesystem.clone(),
161      intermediate_filesystem.clone(),
162      compilation_logging.clone(),
163    );
164    let incremental = Incremental::new_cold(options.incremental);
165    let module_executor = ModuleExecutor::default();
166
167    let id = CompilerId::new();
168    let compiler_context = compiler_context.unwrap_or_else(|| Arc::new(CompilerContext::new()));
169    Self {
170      id,
171      compiler_path,
172      options: options.clone(),
173      compilation: Compilation::new(
174        id,
175        options,
176        platform.clone(),
177        plugin_driver.clone(),
178        buildtime_plugin_driver.clone(),
179        resolver_factory.clone(),
180        loader_resolver_factory.clone(),
181        None,
182        incremental,
183        Some(module_executor),
184        compilation_logging,
185        Default::default(),
186        Default::default(),
187        input_filesystem.clone(),
188        intermediate_filesystem.clone(),
189        output_filesystem.clone(),
190        false,
191        compiler_context.clone(),
192      ),
193      output_filesystem,
194      intermediate_filesystem,
195      plugin_driver,
196      buildtime_plugin_driver,
197      resolver_factory,
198      loader_resolver_factory,
199      cache,
200      emitted_asset_versions: Default::default(),
201      input_filesystem,
202      platform,
203      compiler_context,
204      last_records: None,
205    }
206  }
207
208  pub fn id(&self) -> CompilerId {
209    self.id
210  }
211
212  pub async fn run(&mut self) -> Result<()> {
213    self.build().await?;
214    Ok(())
215  }
216
217  pub async fn build(&mut self) -> Result<()> {
218    let compiler_context = self.compiler_context.clone();
219    match within_compiler_context(compiler_context, self.build_inner()).await {
220      Ok(_) => {
221        self
222          .plugin_driver
223          .compiler_hooks
224          .done
225          .call(&self.compilation)
226          .await?;
227        Ok(())
228      }
229      Err(e) => {
230        self
231          .plugin_driver
232          .compiler_hooks
233          .failed
234          .call(&self.compilation)
235          .await?;
236        Err(e)
237      }
238    }
239  }
240
241  #[instrument("Compiler:build",target=TRACING_BENCH_TARGET, skip_all)]
242  async fn build_inner(&mut self) -> Result<()> {
243    // TODO: clear the outdated cache entries in resolver,
244    // TODO: maybe it's better to use external entries.
245    let plugin_driver_clone = self.plugin_driver.clone();
246    let compilation_id = self.compilation.id();
247    let _guard = scopeguard::guard((), move |_| plugin_driver_clone.clear_cache(compilation_id));
248    let compilation_logging = self.compilation.get_logging().clone();
249    compilation_logging.clear();
250
251    fast_set(
252      &mut self.compilation,
253      Compilation::new(
254        self.id,
255        self.options.clone(),
256        self.platform.clone(),
257        self.plugin_driver.clone(),
258        self.buildtime_plugin_driver.clone(),
259        self.resolver_factory.clone(),
260        self.loader_resolver_factory.clone(),
261        None,
262        Incremental::new_cold(self.options.incremental),
263        Some(Default::default()),
264        compilation_logging,
265        Default::default(),
266        Default::default(),
267        self.input_filesystem.clone(),
268        self.intermediate_filesystem.clone(),
269        self.output_filesystem.clone(),
270        false,
271        self.compiler_context.clone(),
272      ),
273    );
274    let _is_hot = self.cache.before_compile(&mut self.compilation).await;
275    // TODO: disable it for now, enable it once persistent cache is added to all artifacts
276    // if is_hot {
277    //   // If it's a hot start, we can use incremental
278    //   self.compilation.incremental = Incremental::new_hot(self.options.incremental);
279    // }
280
281    self.compile().await?;
282    self.compile_done().await?;
283    self.cache.after_compile(&self.compilation).await;
284    #[cfg(allocative)]
285    crate::utils::snapshot_allocative("build");
286
287    Ok(())
288  }
289  #[instrument("Compiler:compile", target=TRACING_BENCH_TARGET,skip_all)]
290  async fn compile(&mut self) -> Result<()> {
291    let mut compilation_params = self.new_compilation_params();
292    // Make sure `thisCompilation` is emitted before any JS side access to `JsCompilation`.
293    self
294      .plugin_driver
295      .compiler_hooks
296      .this_compilation
297      .call(&mut self.compilation, &mut compilation_params)
298      .await?;
299    self
300      .plugin_driver
301      .compiler_hooks
302      .compilation
303      .call(&mut self.compilation, &mut compilation_params)
304      .await?;
305
306    let logger = self.compilation.get_logger("rspack.Compiler");
307    let start = logger.time("seal compilation");
308    self
309      .compilation
310      .run_passes(self.plugin_driver.clone(), &mut *self.cache)
311      .await?;
312    logger.time_end(start);
313
314    // Consume plugin driver diagnostic
315    let plugin_driver_diagnostics = self.plugin_driver.take_diagnostic();
316    self
317      .compilation
318      .extend_diagnostics(plugin_driver_diagnostics);
319
320    Ok(())
321  }
322
323  #[instrument("Compile:done", skip_all)]
324  async fn compile_done(&mut self) -> Result<()> {
325    let logger = self.compilation.get_logger("rspack.Compiler");
326
327    let should_record = !matches!(
328      self
329        .plugin_driver
330        .compiler_hooks
331        .should_record
332        .call(&mut self.compilation)
333        .await?,
334      Some(false)
335    );
336
337    if should_record {
338      self.last_records = Some(Arc::new(CompilationRecords::record(&self.compilation)));
339    }
340
341    if matches!(
342      self
343        .plugin_driver
344        .compiler_hooks
345        .should_emit
346        .call(&mut self.compilation)
347        .await?,
348      Some(false)
349    ) {
350      return Ok(());
351    }
352
353    let start = logger.time("emitAssets");
354    self.emit_assets().await?;
355    logger.time_end(start);
356
357    Ok(())
358  }
359
360  #[instrument("emit_assets", skip_all)]
361  pub async fn emit_assets(&mut self) -> Result<()> {
362    let output_path_str = self
363      .compilation
364      .get_path(
365        &Filename::from(&self.options.output.path),
366        Default::default(),
367      )
368      .await?;
369    let output_path = Utf8Path::new(&output_path_str);
370    self.run_clean_options(output_path).await?;
371
372    self
373      .plugin_driver
374      .compiler_hooks
375      .emit
376      .call(&mut self.compilation)
377      .await?;
378
379    let mut new_emitted_asset_versions = HashMap::default();
380    let emit_assets_incremental = self
381      .compilation
382      .incremental
383      .passes_enabled(IncrementalPasses::EMIT_ASSETS);
384
385    rspack_parallel::scope(|token| {
386      self
387        .compilation
388        .assets()
389        .iter()
390        .for_each(|(filename, asset)| {
391          // collect version info to new_emitted_asset_versions
392          if emit_assets_incremental {
393            new_emitted_asset_versions.insert(filename.clone(), asset.info.version.clone());
394          }
395
396          if emit_assets_incremental
397            && let Some(old_version) = self.emitted_asset_versions.get(filename)
398            && old_version.as_str() == asset.info.version
399            && !old_version.is_empty()
400          {
401            return;
402          }
403
404          // SAFETY: await immediately and trust caller to poll future entirely
405          let s = unsafe { token.used((&self, filename, asset, output_path)) };
406
407          s.spawn(|(this, filename, asset, output_path)| {
408            this.emit_asset(output_path, filename, asset)
409          });
410        })
411    })
412    .await;
413
414    self.emitted_asset_versions = new_emitted_asset_versions;
415
416    self
417      .plugin_driver
418      .compiler_hooks
419      .after_emit
420      .call(&mut self.compilation)
421      .await
422  }
423  async fn emit_asset(
424    &self,
425    output_path: &Utf8Path,
426    filename: &str,
427    asset: &CompilationAsset,
428  ) -> Result<()> {
429    if let Some(source) = asset.get_source() {
430      let (target_file, query) = filename.split_once('?').unwrap_or((filename, ""));
431      let file_path = output_path.node_join(target_file);
432      self
433        .output_filesystem
434        .create_dir_all(
435          file_path
436            .parent()
437            .unwrap_or_else(|| panic!("The parent of {file_path} can't found")),
438        )
439        .await?;
440
441      let content = source.buffer();
442
443      let mut immutable = asset.info.immutable.unwrap_or(false);
444      if !query.is_empty() {
445        immutable = immutable
446          && (include_hash(target_file, &asset.info.content_hash)
447            || include_hash(target_file, &asset.info.chunk_hash)
448            || include_hash(target_file, &asset.info.full_hash));
449      }
450
451      let stat = self
452        .output_filesystem
453        .stat(file_path.as_path().as_ref())
454        .await
455        .ok();
456
457      let need_write = if !self.options.output.compare_before_emit {
458        // write when compare_before_emit is false
459        true
460      } else if !stat.as_ref().is_some_and(|stat| stat.is_file) {
461        // write when not exists or not a file
462        true
463      } else if immutable {
464        // do not write when asset is immutable and the file exists
465        false
466      } else if (content.len() as u64) == stat.as_ref().unwrap_or_else(|| unreachable!()).size {
467        match self
468          .output_filesystem
469          .read_file(file_path.as_path().as_ref())
470          .await
471        {
472          // write when content is different
473          Ok(c) => content != c,
474          // write when file can not be read
475          Err(_) => true,
476        }
477      } else {
478        // write if content length is different
479        true
480      };
481
482      if need_write {
483        self.output_filesystem.write(&file_path, &content).await?;
484        self.compilation.emitted_assets.insert(filename.to_string());
485      }
486
487      let info = AssetEmittedInfo {
488        output_path: output_path.to_owned(),
489        source: source.clone(),
490        target_path: file_path,
491      };
492      self
493        .plugin_driver
494        .compiler_hooks
495        .asset_emitted
496        .call(&self.compilation, filename, &info)
497        .await?;
498    }
499    Ok(())
500  }
501
502  async fn run_clean_options(&mut self, output_path: &Utf8Path) -> Result<()> {
503    let clean_options = &self.options.output.clean;
504
505    // keep all
506    if let CleanOptions::CleanAll(false) = clean_options {
507      return Ok(());
508    }
509
510    if self.emitted_asset_versions.is_empty() {
511      match clean_options {
512        CleanOptions::CleanAll(true) => {
513          self.output_filesystem.remove_dir_all(output_path).await?;
514        }
515        CleanOptions::KeepPath(p) => {
516          let path = output_path.join(p);
517          trim_dir(
518            &*self.output_filesystem,
519            output_path,
520            KeepPattern::Path(&path),
521          )
522          .await?;
523        }
524        CleanOptions::KeepRegex(r) => {
525          let keep_pattern = KeepPattern::Regex(r);
526          trim_dir(&*self.output_filesystem, output_path, keep_pattern).await?;
527        }
528        CleanOptions::KeepFunc(f) => {
529          let keep_pattern = KeepPattern::Func(f);
530          trim_dir(&*self.output_filesystem, output_path, keep_pattern).await?;
531        }
532        _ => {}
533      }
534
535      return Ok(());
536    }
537
538    let assets = self.compilation.assets();
539    join_all(self.emitted_asset_versions.keys().filter_map(|filename| {
540      if !assets.contains_key(filename) {
541        let filename = filename.to_owned();
542        Some(async {
543          if !clean_options.keep(&filename).await {
544            let filename = output_path.join(filename);
545            let _ = self.output_filesystem.remove_file(&filename).await;
546          }
547        })
548      } else {
549        None
550      }
551    }))
552    .await;
553
554    Ok(())
555  }
556
557  pub fn new_compilation_params(&self) -> CompilationParams {
558    CompilationParams {
559      normal_module_factory: Arc::new(NormalModuleFactory::new(
560        self.options.clone(),
561        self.loader_resolver_factory.clone(),
562        self.plugin_driver.clone(),
563      )),
564      context_module_factory: Arc::new(ContextModuleFactory::new(
565        self.resolver_factory.clone(),
566        self.loader_resolver_factory.clone(),
567        self.plugin_driver.clone(),
568      )),
569    }
570  }
571
572  pub async fn close(&self) -> Result<()> {
573    self
574      .plugin_driver
575      .compiler_hooks
576      .close
577      .call(&self.compilation)
578      .await?;
579
580    self.cache.close().await;
581
582    Ok(())
583  }
584}
585
586#[derive(Debug)]
587pub struct CompilationParams {
588  pub normal_module_factory: Arc<NormalModuleFactory>,
589  pub context_module_factory: Arc<ContextModuleFactory>,
590}
591
592#[derive(Debug)]
593pub struct AssetEmittedInfo {
594  pub source: BoxSource,
595  pub output_path: Utf8PathBuf,
596  pub target_path: Utf8PathBuf,
597}