Skip to main content

rspack_plugin_css/runtime/
mod.rs

1use std::{borrow::Cow, ptr::NonNull};
2
3use rspack_collections::Identifier;
4use rspack_core::{
5  BooleanMatcher, ChunkGroupOrderKey, ChunkUkey, Compilation, CrossOriginLoading, RuntimeGlobals,
6  RuntimeModule, RuntimeModuleStage, RuntimeTemplate, compile_boolean_matcher, impl_runtime_module,
7};
8use rspack_plugin_runtime::{
9  CreateLinkData, LinkPrefetchData, LinkPreloadData, RuntimeModuleChunkWrapper, RuntimePlugin,
10  chunk_has_css, get_chunk_runtime_requirements, stringify_chunks,
11};
12use rustc_hash::FxHashSet as HashSet;
13
14#[impl_runtime_module]
15#[derive(Debug)]
16pub struct CssLoadingRuntimeModule {
17  id: Identifier,
18  chunk: Option<ChunkUkey>,
19}
20
21impl CssLoadingRuntimeModule {
22  pub fn new(runtime_template: &RuntimeTemplate) -> Self {
23    Self::with_default(
24      Identifier::from(format!(
25        "{}css_loading",
26        runtime_template.runtime_module_prefix()
27      )),
28      None,
29    )
30  }
31
32  fn template_id(&self, id: TemplateId) -> String {
33    let base_id = self.id.to_string();
34
35    match id {
36      TemplateId::Raw => base_id,
37      TemplateId::CreateLink => format!("{base_id}_create_link"),
38      TemplateId::WithHmr => format!("{base_id}_with_hmr"),
39      TemplateId::WithLoading => format!("{base_id}_with_loading"),
40      TemplateId::WithPrefetch => format!("{base_id}_with_prefetch"),
41      TemplateId::WithPrefetchLink => format!("{base_id}_with_prefetch_link"),
42      TemplateId::WithPreload => format!("{base_id}_with_preload"),
43      TemplateId::WithPreloadLink => format!("{base_id}_with_preload_link"),
44    }
45  }
46}
47
48enum TemplateId {
49  Raw,
50  CreateLink,
51  WithHmr,
52  WithLoading,
53  WithPrefetch,
54  WithPrefetchLink,
55  WithPreload,
56  WithPreloadLink,
57}
58
59#[async_trait::async_trait]
60impl RuntimeModule for CssLoadingRuntimeModule {
61  fn name(&self) -> Identifier {
62    self.id
63  }
64
65  fn template(&self) -> Vec<(String, String)> {
66    vec![
67      (
68        self.template_id(TemplateId::Raw),
69        include_str!("./css_loading.ejs").to_string(),
70      ),
71      (
72        self.template_id(TemplateId::CreateLink),
73        include_str!("./css_loading_create_link.ejs").to_string(),
74      ),
75      (
76        self.template_id(TemplateId::WithHmr),
77        include_str!("./css_loading_with_hmr.ejs").to_string(),
78      ),
79      (
80        self.template_id(TemplateId::WithLoading),
81        include_str!("./css_loading_with_loading.ejs").to_string(),
82      ),
83      (
84        self.template_id(TemplateId::WithPrefetch),
85        include_str!("./css_loading_with_prefetch.ejs").to_string(),
86      ),
87      (
88        self.template_id(TemplateId::WithPrefetchLink),
89        include_str!("./css_loading_with_prefetch_link.ejs").to_string(),
90      ),
91      (
92        self.template_id(TemplateId::WithPreload),
93        include_str!("./css_loading_with_preload.ejs").to_string(),
94      ),
95      (
96        self.template_id(TemplateId::WithPreloadLink),
97        include_str!("./css_loading_with_preload_link.ejs").to_string(),
98      ),
99    ]
100  }
101
102  async fn generate(&self, compilation: &Compilation) -> rspack_error::Result<String> {
103    if let Some(chunk_ukey) = self.chunk {
104      let runtime_hooks = RuntimePlugin::get_compilation_hooks(compilation.id());
105      let chunk = compilation.chunk_by_ukey.expect_get(&chunk_ukey);
106      let runtime_requirements = get_chunk_runtime_requirements(compilation, &chunk_ukey);
107
108      let unique_name = &compilation.options.output.unique_name;
109      let with_hmr = runtime_requirements.contains(RuntimeGlobals::HMR_DOWNLOAD_UPDATE_HANDLERS);
110
111      let condition_map =
112        compilation
113          .chunk_graph
114          .get_chunk_condition_map(&chunk_ukey, compilation, chunk_has_css);
115      let has_css_matcher = compile_boolean_matcher(&condition_map);
116
117      let with_loading = runtime_requirements.contains(RuntimeGlobals::ENSURE_CHUNK_HANDLERS)
118        && !matches!(has_css_matcher, BooleanMatcher::Condition(false));
119      let with_fetch_priority = runtime_requirements.contains(RuntimeGlobals::HAS_FETCH_PRIORITY);
120
121      let initial_chunks = chunk.get_all_initial_chunks(&compilation.chunk_group_by_ukey);
122      let mut initial_chunk_ids = HashSet::default();
123
124      for chunk_ukey in initial_chunks.iter() {
125        let id = compilation
126          .chunk_by_ukey
127          .expect_get(chunk_ukey)
128          .expect_id()
129          .clone();
130        if chunk_has_css(chunk_ukey, compilation) {
131          initial_chunk_ids.insert(id);
132        }
133      }
134
135      let environment = &compilation.options.output.environment;
136      let is_neutral_platform = compilation.platform.is_neutral();
137      let with_prefetch = runtime_requirements.contains(RuntimeGlobals::PREFETCH_CHUNK_HANDLERS)
138        && (environment.supports_document() || is_neutral_platform)
139        && chunk.has_child_by_order(
140          compilation,
141          &ChunkGroupOrderKey::Prefetch,
142          true,
143          &chunk_has_css,
144        );
145      let with_preload = runtime_requirements.contains(RuntimeGlobals::PRELOAD_CHUNK_HANDLERS)
146        && (environment.supports_document() || is_neutral_platform)
147        && chunk.has_child_by_order(
148          compilation,
149          &ChunkGroupOrderKey::Preload,
150          true,
151          &chunk_has_css,
152        );
153
154      if !with_hmr && !with_loading {
155        return Ok("".to_string());
156      }
157
158      let mut source = String::new();
159      // object to store loaded and loading chunks
160      // undefined = chunk not loaded, null = chunk preloaded/prefetched
161      // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
162
163      // One entry initial chunk maybe is other entry dynamic chunk, so here
164      // only render chunk without css. See packages/rspack/tests/runtimeCases/runtime/split-css-chunk test.
165      source.push_str(&format!(
166        "var installedChunks = {};\n",
167        &stringify_chunks(&initial_chunk_ids, 0)
168      ));
169
170      let create_link_raw = compilation.runtime_template.render(
171        &self.template_id(TemplateId::CreateLink),
172        Some(serde_json::json!({
173          "_with_fetch_priority": with_fetch_priority,
174          "_charset": compilation.options.output.charset,
175          "_cross_origin": match &compilation.options.output.cross_origin_loading {
176            CrossOriginLoading::Disable => "".to_string(),
177            CrossOriginLoading::Enable(cross_origin) => cross_origin.to_string(),
178          },
179          "_unique_name": unique_name,
180        })),
181      )?;
182
183      let create_link = runtime_hooks
184        .borrow()
185        .create_link
186        .call(CreateLinkData {
187          code: create_link_raw,
188          chunk: RuntimeModuleChunkWrapper {
189            chunk_ukey,
190            compilation_id: compilation.id(),
191            compilation: NonNull::from(compilation),
192          },
193        })
194        .await?;
195
196      let chunk_load_timeout = compilation.options.output.chunk_load_timeout.to_string();
197
198      let load_css_chunk_data = compilation.runtime_template.basic_function(
199        "target, chunkId",
200        &format!(
201          r#"{}
202installedChunks[chunkId] = 0;
203{}"#,
204          with_hmr
205            .then_some(format!(
206              "var moduleIds = [];\nif(target == {})",
207              compilation
208                .runtime_template
209                .render_runtime_globals(&RuntimeGlobals::MODULE_FACTORIES)
210            ))
211            .unwrap_or_default(),
212          if with_hmr {
213            "return moduleIds"
214          } else {
215            Default::default()
216          },
217        ),
218      );
219      let load_initial_chunk_data = if initial_chunk_ids.len() > 2 {
220        Cow::Owned(format!(
221          "[{}].forEach(loadCssChunkData.bind(null, {}, 0));",
222          initial_chunk_ids
223            .iter()
224            .map(|id| serde_json::to_string(id).expect("should ok to convert to string"))
225            .collect::<Vec<_>>()
226            .join(","),
227          compilation
228            .runtime_template
229            .render_runtime_globals(&RuntimeGlobals::MODULE_FACTORIES)
230        ))
231      } else if !initial_chunk_ids.is_empty() {
232        Cow::Owned(
233          initial_chunk_ids
234            .iter()
235            .map(|id| {
236              let id = serde_json::to_string(id).expect("should ok to convert to string");
237              format!(
238                "loadCssChunkData({}, 0, {});",
239                compilation
240                  .runtime_template
241                  .render_runtime_globals(&RuntimeGlobals::MODULE_FACTORIES),
242                id
243              )
244            })
245            .collect::<Vec<_>>()
246            .join(""),
247        )
248      } else {
249        Cow::Borrowed("// no initial css")
250      };
251
252      let raw_source = compilation.runtime_template.render(
253        &self.template_id(TemplateId::Raw),
254        Some(serde_json::json!({
255          "_unique_name": unique_name,
256          "_css_chunk_data": &load_css_chunk_data,
257          "_create_link": &create_link.code,
258          "_chunk_load_timeout": &chunk_load_timeout,
259          "_initial_css_chunk_data": &load_initial_chunk_data,
260        })),
261      )?;
262      source.push_str(&raw_source);
263
264      if with_loading {
265        let source_with_loading = compilation.runtime_template.render(
266          &self.template_id(TemplateId::WithLoading),
267          Some(serde_json::json!({
268            "_css_matcher": &has_css_matcher.render("chunkId"),
269            "_is_neutral_platform": is_neutral_platform
270          })),
271        )?;
272        source.push_str(&source_with_loading);
273      }
274
275      if with_prefetch && !matches!(has_css_matcher, BooleanMatcher::Condition(false)) {
276        let link_prefetch_raw = compilation.runtime_template.render(
277          &self.template_id(TemplateId::WithPrefetchLink),
278          Some(serde_json::json!({
279            "_charset": compilation.options.output.charset,
280            "_cross_origin": compilation.options.output.cross_origin_loading.to_string(),
281          })),
282        )?;
283
284        let link_prefetch = runtime_hooks
285          .borrow()
286          .link_prefetch
287          .call(LinkPrefetchData {
288            code: link_prefetch_raw,
289            chunk: RuntimeModuleChunkWrapper {
290              chunk_ukey,
291              compilation_id: compilation.id(),
292              compilation: NonNull::from(compilation),
293            },
294          })
295          .await?;
296
297        let source_with_prefetch = compilation.runtime_template.render(
298          &self.template_id(TemplateId::WithPrefetch),
299          Some(serde_json::json!({
300            "_css_matcher": &has_css_matcher.render("chunkId"),
301            "_create_prefetch_link": &link_prefetch.code,
302            "_is_neutral_platform": is_neutral_platform
303          })),
304        )?;
305        source.push_str(&source_with_prefetch);
306      }
307
308      if with_preload && !matches!(has_css_matcher, BooleanMatcher::Condition(false)) {
309        let link_preload_raw = compilation.runtime_template.render(
310          &self.template_id(TemplateId::WithPreloadLink),
311          Some(serde_json::json!({
312            "_charset": compilation.options.output.charset,
313            "_cross_origin": compilation.options.output.cross_origin_loading.to_string(),
314          })),
315        )?;
316
317        let link_preload = runtime_hooks
318          .borrow()
319          .link_preload
320          .call(LinkPreloadData {
321            code: link_preload_raw,
322            chunk: RuntimeModuleChunkWrapper {
323              chunk_ukey,
324              compilation_id: compilation.id(),
325              compilation: NonNull::from(compilation),
326            },
327          })
328          .await?;
329
330        let source_with_preload = compilation.runtime_template.render(
331          &self.template_id(TemplateId::WithPreload),
332          Some(serde_json::json!({
333            "_css_matcher": &has_css_matcher.render("chunkId"),
334            "_create_preload_link": &link_preload.code,
335            "_is_neutral_platform": is_neutral_platform
336          })),
337        )?;
338        source.push_str(&source_with_preload);
339      }
340
341      if with_hmr {
342        let source_with_hmr = compilation.runtime_template.render(
343          &self.template_id(TemplateId::WithHmr),
344          Some(serde_json::json!({
345            "_is_neutral_platform": is_neutral_platform
346          })),
347        )?;
348        source.push_str(&source_with_hmr);
349      }
350
351      Ok(source)
352    } else {
353      unreachable!("should attach chunk for css_loading")
354    }
355  }
356
357  fn attach(&mut self, chunk: ChunkUkey) {
358    self.chunk = Some(chunk);
359  }
360
361  fn stage(&self) -> RuntimeModuleStage {
362    RuntimeModuleStage::Attach
363  }
364}