rspack_plugin_css/runtime/
mod.rs1use 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 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}