rspack_plugin_extract_css/
plugin.rs

1use std::{
2  borrow::Cow,
3  hash::Hash,
4  sync::{Arc, LazyLock},
5};
6
7use cow_utils::CowUtils;
8use regex::Regex;
9use rspack_cacheable::cacheable;
10use rspack_collections::{DatabaseItem, IdentifierMap, IdentifierSet, UkeySet};
11use rspack_core::{
12  AssetInfo, Chunk, ChunkGraph, ChunkGroupUkey, ChunkKind, ChunkUkey, Compilation,
13  CompilationContentHash, CompilationParams, CompilationRenderManifest,
14  CompilationRuntimeRequirementInTree, CompilerCompilation, DependencyType, Filename, Module,
15  ModuleGraph, ModuleIdentifier, ModuleType, NormalModuleFactoryParser, ParserAndGenerator,
16  ParserOptions, PathData, Plugin, RenderManifestEntry, RuntimeGlobals, SourceType, get_undo_path,
17  rspack_sources::{
18    BoxSource, CachedSource, ConcatSource, RawStringSource, SourceExt, SourceMap, SourceMapSource,
19    WithoutOriginalOptions,
20  },
21};
22use rspack_error::{Diagnostic, Result};
23use rspack_hash::RspackHash;
24use rspack_hook::{plugin, plugin_hook};
25use rspack_plugin_javascript::{
26  BoxJavascriptParserPlugin, parser_and_generator::JavaScriptParserAndGenerator,
27};
28use rspack_plugin_runtime::GetChunkFilenameRuntimeModule;
29use rustc_hash::FxHashMap;
30use ustr::Ustr;
31
32use crate::{
33  css_module::{CssModule, CssModuleFactory},
34  parser_plugin::PluginCssExtractParserPlugin,
35  runtime::CssLoadingRuntimeModule,
36};
37pub static PLUGIN_NAME: &str = "css-extract-rspack-plugin";
38
39pub static MODULE_TYPE_STR: LazyLock<Ustr> = LazyLock::new(|| Ustr::from("css/mini-extract"));
40pub static MODULE_TYPE: LazyLock<ModuleType> =
41  LazyLock::new(|| ModuleType::Custom(*MODULE_TYPE_STR));
42pub static SOURCE_TYPE: LazyLock<[SourceType; 1]> =
43  LazyLock::new(|| [SourceType::Custom(*MODULE_TYPE_STR)]);
44
45pub static BASE_URI: &str = "webpack://";
46pub static ABSOLUTE_PUBLIC_PATH: &str = "webpack:///mini-css-extract-plugin/";
47pub static AUTO_PUBLIC_PATH: &str = "__mini_css_extract_plugin_public_path_auto__";
48pub static SINGLE_DOT_PATH_SEGMENT: &str = "__mini_css_extract_plugin_single_dot_path_segment__";
49
50static STARTS_WITH_AT_IMPORT: &str = "@import url";
51
52struct CssOrderConflicts {
53  chunk: ChunkUkey,
54  fallback_module: ModuleIdentifier,
55
56  // (module, failed chunkGroups, fulfilled chunkGroups)
57  reasons: Vec<(ModuleIdentifier, Option<String>, Option<String>)>,
58}
59
60#[plugin]
61#[derive(Debug)]
62pub struct PluginCssExtract {
63  pub(crate) options: Arc<CssExtractOptions>,
64}
65
66impl Eq for PluginCssExtractInner {}
67
68impl PartialEq for PluginCssExtractInner {
69  fn eq(&self, other: &Self) -> bool {
70    Arc::ptr_eq(&self.options, &other.options)
71  }
72}
73
74#[derive(Debug)]
75pub struct CssExtractOptions {
76  pub filename: Filename,
77  pub chunk_filename: Filename,
78  pub ignore_order: bool,
79  pub insert: InsertType,
80  pub attributes: FxHashMap<String, String>,
81  pub link_type: Option<String>,
82  pub runtime: bool,
83  pub pathinfo: bool,
84  pub enforce_relative: bool,
85}
86
87// impl PartialEq for CssExtractOptions {
88//   fn eq(&self, other: &Self) -> bool {
89//     let equal = self.ignore_order == other.ignore_order
90//       && self.insert == other.insert
91//       && self.attributes == other.attributes
92//       && self.link_type == other.link_type
93//       && self.runtime == other.runtime
94//       && self.pathinfo == other.pathinfo;
95
96//     if !equal {
97//       return false;
98//     }
99
100//     // TODO: function eq
101//     match (self.filename.template(), self.chunk_filename.template()) {
102//       (None, None) => return true,
103//       (None, Some(_)) => return false,
104//       (Some(_), None) => return false,
105//       (Some(a), Some(b)) => a == b,
106//     }
107//   }
108// }
109
110#[cacheable]
111#[derive(Debug, PartialEq, Eq, Clone)]
112pub enum InsertType {
113  Fn(String),
114  Selector(String),
115  Default,
116}
117
118impl PluginCssExtract {
119  pub fn new(options: CssExtractOptions) -> Self {
120    Self::new_inner(Arc::new(options))
121  }
122
123  // port from https://github.com/webpack-contrib/mini-css-extract-plugin/blob/d5e540baf8280442e523530ebbbe31c57a4c4336/src/index.js#L1127
124  fn sort_modules<'comp>(
125    &self,
126    chunk: &Chunk,
127    modules: &[&dyn Module],
128    compilation: &'comp Compilation,
129    module_graph: &'comp ModuleGraph<'comp>,
130  ) -> (Vec<&'comp dyn Module>, Option<Vec<CssOrderConflicts>>) {
131    let mut module_deps_reasons: IdentifierMap<IdentifierMap<UkeySet<ChunkGroupUkey>>> = modules
132      .iter()
133      .map(|m| (m.identifier(), Default::default()))
134      .collect();
135
136    let mut module_dependencies: IdentifierMap<IdentifierSet> = modules
137      .iter()
138      .map(|module| (module.identifier(), IdentifierSet::default()))
139      .collect();
140
141    let mut groups = chunk.groups().iter().cloned().collect::<Vec<_>>();
142    groups.sort_by(|a, b| {
143      let a = compilation.chunk_group_by_ukey.expect_get(a);
144      let b = compilation.chunk_group_by_ukey.expect_get(b);
145      match a.index.cmp(&b.index) {
146        std::cmp::Ordering::Equal => a.ukey.cmp(&b.ukey),
147        order_res => order_res,
148      }
149    });
150
151    let mut modules_by_chunk_group = groups
152      .iter()
153      .map(|chunk_group| {
154        let chunk_group = compilation.chunk_group_by_ukey.expect_get(chunk_group);
155        let mut sorted_module = modules
156          .iter()
157          .map(|module| {
158            let identifier = module.identifier();
159            (identifier, chunk_group.module_post_order_index(&identifier))
160          })
161          .filter_map(|(id, idx)| idx.map(|idx| (id, idx)))
162          .collect::<Vec<_>>();
163
164        sorted_module.sort_by(|(_, idx1), (_, idx2)| idx2.cmp(idx1));
165
166        for (i, (module, _)) in sorted_module.iter().enumerate() {
167          let set = module_dependencies
168            .get_mut(module)
169            .expect("should have module before");
170
171          let reasons = module_deps_reasons
172            .get_mut(module)
173            .expect("should have module dep reason");
174
175          let mut j = i + 1;
176          while j < sorted_module.len() {
177            let (module, _) = sorted_module[j];
178            set.insert(module);
179
180            let reason = reasons.entry(module).or_default();
181            reason.insert(chunk_group.ukey);
182
183            j += 1;
184          }
185        }
186
187        sorted_module
188      })
189      .collect::<Vec<Vec<(ModuleIdentifier, usize)>>>();
190
191    let mut used_modules: IdentifierSet = Default::default();
192    let mut result: Vec<&dyn Module> = Default::default();
193    let mut conflicts: Option<Vec<CssOrderConflicts>> = None;
194
195    while used_modules.len() < modules.len() {
196      let mut success = false;
197      let mut best_match: Option<Vec<ModuleIdentifier>> = None;
198      let mut best_match_deps: Option<Vec<ModuleIdentifier>> = None;
199
200      for list in &mut modules_by_chunk_group {
201        // skip and remove already added modules
202        while !list.is_empty()
203          && used_modules.contains(&list.last().expect("should have list item").0)
204        {
205          list.pop();
206        }
207
208        // skip empty lists
209        if !list.is_empty() {
210          let module = list.last().expect("should have item").0;
211          let deps = module_dependencies.get(&module).expect("should have deps");
212          let failed_deps = deps
213            .iter()
214            .filter(|dep| !used_modules.contains(dep))
215            .cloned()
216            .collect::<Vec<_>>();
217
218          let failed_count = failed_deps.len();
219
220          if best_match_deps.is_none()
221            || best_match_deps
222              .as_ref()
223              .expect("should have best match dep")
224              .len()
225              > failed_deps.len()
226          {
227            best_match = Some(list.iter().map(|(id, _)| *id).collect());
228            best_match_deps = Some(failed_deps);
229          }
230
231          if failed_count == 0 {
232            list.pop();
233            used_modules.insert(module);
234            result.push(
235              module_graph
236                .module_by_identifier(&module)
237                .expect("should have module")
238                .as_ref(),
239            );
240            success = true;
241            break;
242          }
243        }
244      }
245
246      if !success {
247        // no module found => there is a conflict
248        // use list with fewest failed deps
249        // and emit a warning
250        let mut best_match = best_match.expect("should have best match");
251        let best_match_deps = best_match_deps.expect("should have best match");
252        let fallback_module = best_match.pop().expect("should have best match");
253        if !self.options.ignore_order {
254          let reasons = module_deps_reasons
255            .get(&fallback_module)
256            .expect("should have dep reason");
257
258          let new_conflict = CssOrderConflicts {
259            chunk: chunk.ukey(),
260            fallback_module,
261            reasons: best_match_deps
262              .into_iter()
263              .map(|m| {
264                let good_reasons_map = module_deps_reasons.get(&m);
265                let good_reasons =
266                  good_reasons_map.and_then(|reasons| reasons.get(&fallback_module));
267
268                let failed_chunk_groups = reasons.get(&m).map(|reasons| {
269                  reasons
270                    .iter()
271                    .filter_map(|cg| {
272                      let chunk_group = compilation.chunk_group_by_ukey.expect_get(cg);
273
274                      chunk_group.name()
275                    })
276                    .collect::<Vec<_>>()
277                    .join(",")
278                });
279
280                let good_chunk_groups = good_reasons.map(|reasons| {
281                  reasons
282                    .iter()
283                    .filter_map(|cg| compilation.chunk_group_by_ukey.expect_get(cg).name())
284                    .collect::<Vec<_>>()
285                    .join(", ")
286                });
287
288                (m, failed_chunk_groups, good_chunk_groups)
289              })
290              .collect(),
291          };
292          if let Some(conflicts) = &mut conflicts {
293            conflicts.push(new_conflict);
294          } else {
295            conflicts = Some(vec![new_conflict]);
296          }
297        }
298
299        used_modules.insert(fallback_module);
300        result.push(
301          module_graph
302            .module_by_identifier(&fallback_module)
303            .expect("should have fallback module")
304            .as_ref(),
305        );
306      }
307    }
308
309    (result, conflicts)
310  }
311
312  async fn render_content_asset(
313    &self,
314    chunk: &Chunk,
315    rendered_modules: &[&dyn Module],
316    filename: &str,
317    compilation: &'_ Compilation,
318  ) -> (BoxSource, Vec<Diagnostic>) {
319    let module_graph = compilation.get_module_graph();
320    // mini-extract-plugin has different conflict order in some cases,
321    // for compatibility, we cannot use experiments.css sorting algorithm
322    let (used_modules, conflicts) =
323      self.sort_modules(chunk, rendered_modules, compilation, &module_graph);
324
325    let mut diagnostics = Vec::new();
326    if let Some(conflicts) = conflicts {
327      diagnostics.extend(conflicts.into_iter().map(|conflict| {
328        let chunk = compilation.chunk_by_ukey.expect_get(&conflict.chunk);
329        let fallback_module = module_graph
330          .module_by_identifier(&conflict.fallback_module)
331          .expect("should have module");
332
333        let mut diagnostic = Diagnostic::warn(
334          "".into(),
335          format!(
336            r#"chunk {} [{PLUGIN_NAME}]
337Conflicting order. Following module has been added:
338 * {}
339despite it was not able to fulfill desired ordering with these modules:
340{}"#,
341            chunk
342              .name()
343              .or_else(|| chunk
344                .id(&compilation.chunk_ids_artifact)
345                .map(|id| id.as_str()))
346              .unwrap_or_default(),
347            fallback_module.readable_identifier(&compilation.options.context),
348            conflict
349              .reasons
350              .iter()
351              .map(|(m, failed_reasons, good_reasons)| {
352                let m = module_graph
353                  .module_by_identifier(m)
354                  .expect("should have module");
355
356                format!(
357                  " * {}\n  - couldn't fulfill desired order of chunk group(s) {}{}",
358                  m.readable_identifier(&compilation.options.context),
359                  failed_reasons
360                    .as_ref()
361                    .map(|s| s.as_str())
362                    .unwrap_or_default(),
363                  good_reasons
364                    .as_ref()
365                    .map(|s| format!(
366                      "\n  - while fulfilling desired order of chunk group(s) {}",
367                      s.as_str()
368                    ))
369                    .unwrap_or_default(),
370                )
371              })
372              .collect::<Vec<_>>()
373              .join("\n")
374          ),
375        );
376        diagnostic.file = Some(filename.to_owned().into());
377        diagnostic.chunk = Some(chunk.ukey().as_u32());
378        diagnostic
379      }));
380    }
381
382    let used_modules = used_modules
383      .into_iter()
384      .filter_map(|module| module.downcast_ref::<CssModule>());
385
386    let mut source = ConcatSource::default();
387    let mut external_source = ConcatSource::default();
388
389    for module in used_modules {
390      let content = Cow::Borrowed(module.content.as_str());
391      let readable_identifier = module.readable_identifier(&compilation.options.context);
392      let starts_with_at_import = content.starts_with(STARTS_WITH_AT_IMPORT);
393
394      let header = self.options.pathinfo.then(|| {
395        let req_str = readable_identifier.cow_replace("*/", "*_/");
396        let req_str_star = "*".repeat(req_str.len());
397        RawStringSource::from(format!(
398          "/*!****{req_str_star}****!*\\\n  !*** {req_str} ***!\n  \\****{req_str_star}****/\n"
399        ))
400      });
401
402      if starts_with_at_import {
403        if let Some(header) = header {
404          external_source.add(header);
405        }
406        if let Some(media) = &module.media {
407          static MEDIA_RE: LazyLock<Regex> =
408            LazyLock::new(|| Regex::new(r#";|\s*$"#).expect("should compile"));
409          let new_content = MEDIA_RE.replace_all(content.as_ref(), media);
410          external_source.add(RawStringSource::from(new_content.to_string() + "\n"));
411        } else {
412          external_source.add(RawStringSource::from(content.to_string() + "\n"));
413        }
414      } else {
415        let mut need_supports = false;
416        let mut need_media = false;
417
418        if let Some(header) = header {
419          source.add(header);
420        }
421
422        if let Some(supports) = &module.supports
423          && !supports.is_empty()
424        {
425          need_supports = true;
426          source.add(RawStringSource::from(format!(
427            "@supports ({supports}) {{\n"
428          )));
429        }
430
431        if let Some(media) = &module.media
432          && !media.is_empty()
433        {
434          need_media = true;
435          source.add(RawStringSource::from(format!("@media {media} {{\n")));
436        }
437
438        if let Some(layer) = &module.css_layer {
439          source.add(RawStringSource::from(format!("@layer {layer} {{\n")));
440        }
441
442        // different from webpack, add `enforce_relative` to preserve './'
443        let undo_path = get_undo_path(
444          filename,
445          compilation.options.output.path.to_string(),
446          self.options.enforce_relative,
447        );
448
449        let content = content.cow_replace(ABSOLUTE_PUBLIC_PATH, "");
450        let content = content.cow_replace(SINGLE_DOT_PATH_SEGMENT, ".");
451        let content = content.cow_replace(AUTO_PUBLIC_PATH, &undo_path);
452        let content = content.cow_replace(
453          BASE_URI,
454          chunk
455            .get_entry_options(&compilation.chunk_group_by_ukey)
456            .and_then(|entry_options| entry_options.base_uri.as_ref())
457            .unwrap_or(&undo_path),
458        );
459
460        if let Some(source_map) = &module.source_map {
461          source.add(SourceMapSource::new(WithoutOriginalOptions {
462            value: content.to_string(),
463            name: readable_identifier,
464            source_map: SourceMap::from_json(source_map).expect("invalid sourcemap"),
465          }))
466        } else {
467          source.add(RawStringSource::from(content.to_string()));
468        }
469
470        source.add(RawStringSource::from_static("\n"));
471
472        if need_media {
473          source.add(RawStringSource::from_static("}\n"));
474        }
475
476        if need_supports {
477          source.add(RawStringSource::from_static("}\n"));
478        }
479
480        if module.css_layer.is_some() {
481          source.add(RawStringSource::from_static("}\n"));
482        }
483      }
484    }
485
486    external_source.add(source);
487    (external_source.boxed(), diagnostics)
488  }
489}
490
491#[plugin_hook(CompilerCompilation for PluginCssExtract)]
492async fn compilation(
493  &self,
494  compilation: &mut Compilation,
495  _params: &mut CompilationParams,
496) -> Result<()> {
497  compilation.set_dependency_factory(DependencyType::ExtractCSS, Arc::new(CssModuleFactory));
498  Ok(())
499}
500
501#[plugin_hook(CompilationRuntimeRequirementInTree for PluginCssExtract)]
502async fn runtime_requirement_in_tree(
503  &self,
504  compilation: &mut Compilation,
505  chunk_ukey: &ChunkUkey,
506  _all_runtime_requirements: &RuntimeGlobals,
507  runtime_requirements: &RuntimeGlobals,
508  runtime_requirements_mut: &mut RuntimeGlobals,
509) -> Result<Option<()>> {
510  // different from webpack, Rspack can invoke this multiple times,
511  // each time with current runtime_globals, and records every mutation
512  // by `runtime_requirements_mut`, but this RuntimeModule depends on
513  // 2 runtimeGlobals, if check current runtime_requirements, we might
514  // insert CssLoadingRuntimeModule with with_loading: true but with_hmr: false
515  // for the first time, and with_loading: false but with_hmr: true for the
516  // second time
517  // For plugin that depends on 2 runtime_globals, should check all_runtime_requirements
518  if !self.options.runtime {
519    return Ok(None);
520  }
521
522  let has_hot_update = runtime_requirements.contains(RuntimeGlobals::HMR_DOWNLOAD_UPDATE_HANDLERS);
523
524  if has_hot_update || runtime_requirements.contains(RuntimeGlobals::ENSURE_CHUNK_HANDLERS) {
525    if self.options.chunk_filename.has_hash_placeholder() {
526      runtime_requirements_mut.insert(RuntimeGlobals::GET_FULL_HASH);
527    }
528
529    runtime_requirements_mut.insert(RuntimeGlobals::PUBLIC_PATH);
530
531    let filename = self.options.filename.clone();
532    let chunk_filename = self.options.chunk_filename.clone();
533
534    compilation.add_runtime_module(
535      chunk_ukey,
536      Box::new(GetChunkFilenameRuntimeModule::new(
537        "css",
538        "mini-css",
539        SOURCE_TYPE[0],
540        "__webpack_require__.miniCssF".into(),
541        move |runtime_requirements| {
542          runtime_requirements.contains(RuntimeGlobals::HMR_DOWNLOAD_UPDATE_HANDLERS)
543        },
544        move |chunk, compilation| {
545          chunk
546            .content_hash(&compilation.chunk_hashes_artifact)?
547            .contains_key(&SOURCE_TYPE[0])
548            .then(|| {
549              if chunk.can_be_initial(&compilation.chunk_group_by_ukey) {
550                filename.clone()
551              } else {
552                chunk_filename.clone()
553              }
554            })
555        },
556      )),
557    )?;
558
559    compilation.add_runtime_module(
560      chunk_ukey,
561      Box::new(CssLoadingRuntimeModule::new(
562        *chunk_ukey,
563        self.options.attributes.clone(),
564        self.options.link_type.clone(),
565        self.options.insert.clone(),
566      )),
567    )?;
568  }
569
570  Ok(None)
571}
572
573#[plugin_hook(CompilationContentHash for PluginCssExtract)]
574async fn content_hash(
575  &self,
576  compilation: &Compilation,
577  chunk_ukey: &ChunkUkey,
578  hashes: &mut FxHashMap<SourceType, RspackHash>,
579) -> Result<()> {
580  let module_graph = compilation.get_module_graph();
581
582  let rendered_modules = compilation.chunk_graph.get_chunk_modules_by_source_type(
583    chunk_ukey,
584    SOURCE_TYPE[0],
585    &module_graph,
586  );
587
588  if rendered_modules.is_empty() {
589    return Ok(());
590  }
591  let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey);
592
593  let (used_modules, diagnostics) =
594    self.sort_modules(chunk, &rendered_modules, compilation, &module_graph);
595
596  let hasher = hashes
597    .entry(SOURCE_TYPE[0])
598    .or_insert_with(|| RspackHash::from(&compilation.options.output));
599
600  used_modules
601    .iter()
602    .map(|m| ChunkGraph::get_module_hash(compilation, m.identifier(), chunk.runtime()))
603    .for_each(|current| current.hash(hasher));
604
605  " ".hash(hasher);
606  if let Some(diagnostics) = diagnostics {
607    diagnostics.iter().for_each(|curr| {
608      curr.fallback_module.hash(hasher);
609    });
610  }
611
612  Ok(())
613}
614
615#[plugin_hook(CompilationRenderManifest for PluginCssExtract)]
616async fn render_manifest(
617  &self,
618  compilation: &Compilation,
619  chunk_ukey: &ChunkUkey,
620  manifest: &mut Vec<RenderManifestEntry>,
621  diagnostics: &mut Vec<Diagnostic>,
622) -> Result<()> {
623  let module_graph = compilation.get_module_graph();
624  let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey);
625
626  if matches!(chunk.kind(), ChunkKind::HotUpdate) {
627    return Ok(());
628  }
629
630  let rendered_modules = compilation.chunk_graph.get_chunk_modules_by_source_type(
631    chunk_ukey,
632    SOURCE_TYPE[0],
633    &module_graph,
634  );
635
636  if rendered_modules.is_empty() {
637    return Ok(());
638  }
639
640  let filename_template = if chunk.can_be_initial(&compilation.chunk_group_by_ukey) {
641    &self.options.filename
642  } else {
643    &self.options.chunk_filename
644  };
645
646  let mut asset_info = AssetInfo::default();
647  let filename = compilation
648    .get_path_with_info(
649      filename_template,
650      PathData::default()
651        .chunk_id_optional(
652          chunk
653            .id(&compilation.chunk_ids_artifact)
654            .map(|id| id.as_str()),
655        )
656        .chunk_hash_optional(chunk.rendered_hash(
657          &compilation.chunk_hashes_artifact,
658          compilation.options.output.hash_digest_length,
659        ))
660        .chunk_name_optional(chunk.name_for_filename_template(&compilation.chunk_ids_artifact))
661        .content_hash_optional(chunk.rendered_content_hash_by_source_type(
662          &compilation.chunk_hashes_artifact,
663          &SOURCE_TYPE[0],
664          compilation.options.output.hash_digest_length,
665        )),
666      &mut asset_info,
667    )
668    .await?;
669
670  let (source, more_diagnostics) = compilation
671    .chunk_render_cache_artifact
672    .use_cache(compilation, chunk, &SOURCE_TYPE[0], || async {
673      let (source, diagnostics) = self
674        .render_content_asset(chunk, &rendered_modules, &filename, compilation)
675        .await;
676      Ok((CachedSource::new(source).boxed(), diagnostics))
677    })
678    .await?;
679
680  diagnostics.extend(more_diagnostics);
681  manifest.push(RenderManifestEntry {
682    source,
683    filename,
684    has_filename: false,
685    info: asset_info,
686    auxiliary: false,
687  });
688
689  Ok(())
690}
691
692#[plugin_hook(NormalModuleFactoryParser for PluginCssExtract)]
693async fn nmf_parser(
694  &self,
695  module_type: &ModuleType,
696  parser: &mut dyn ParserAndGenerator,
697  _parser_options: Option<&ParserOptions>,
698) -> Result<()> {
699  if module_type.is_js_like()
700    && let Some(parser) = parser.downcast_mut::<JavaScriptParserAndGenerator>()
701  {
702    parser.add_parser_plugin(
703      Box::<PluginCssExtractParserPlugin>::default() as BoxJavascriptParserPlugin
704    );
705  }
706  Ok(())
707}
708
709impl Plugin for PluginCssExtract {
710  fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
711    ctx.compiler_hooks.compilation.tap(compilation::new(self));
712    ctx
713      .compilation_hooks
714      .runtime_requirement_in_tree
715      .tap(runtime_requirement_in_tree::new(self));
716    ctx
717      .compilation_hooks
718      .content_hash
719      .tap(content_hash::new(self));
720    ctx
721      .compilation_hooks
722      .render_manifest
723      .tap(render_manifest::new(self));
724
725    ctx
726      .normal_module_factory_hooks
727      .parser
728      .tap(nmf_parser::new(self));
729
730    Ok(())
731  }
732}