1use std::{hash::Hash, sync::LazyLock};
2
3use itertools::Itertools;
4use regex::Regex;
5use rspack_collections::IdentifierLinkedMap;
6use rspack_core::{
7 Chunk, ChunkGraph, ChunkGroupByUkey, ChunkGroupUkey, ChunkLoading, ChunkLoadingType, ChunkUkey,
8 Compilation, PathData, RuntimeCodeTemplate, RuntimeGlobals, RuntimeVariable, SourceType,
9 chunk_graph_chunk::ChunkIdSet,
10 get_js_chunk_filename_template,
11 rspack_sources::{BoxSource, RawStringSource, SourceExt},
12};
13use rspack_error::{Result, error};
14use rspack_hash::RspackHash;
15use rspack_plugin_javascript::runtime::stringify_chunks_to_array;
16use rspack_util::fx_hash::FxIndexSet;
17use rustc_hash::FxHashSet as HashSet;
18
19use crate::runtime_module::is_enabled_for_chunk;
20
21pub fn should_export_webpack_require_for_module_chunk_loading(
22 chunk_ukey: &ChunkUkey,
23 compilation: &Compilation,
24) -> bool {
25 let chunk_loading = ChunkLoading::Enable(ChunkLoadingType::Import);
26 is_enabled_for_chunk(chunk_ukey, &chunk_loading, compilation)
27 && compilation
28 .build_chunk_graph_artifact
29 .chunk_graph
30 .has_chunk_entry_dependent_chunks(
31 chunk_ukey,
32 &compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
33 )
34}
35
36pub fn update_hash_for_entry_startup(
37 hasher: &mut RspackHash,
38 compilation: &Compilation,
39 entries: &IdentifierLinkedMap<ChunkGroupUkey>,
40 chunk: &ChunkUkey,
41) {
42 for (module, entry) in entries {
43 if let Some(module_id) = compilation
44 .get_module_graph()
45 .module_graph_module_by_identifier(module)
46 .and_then(|module| {
47 ChunkGraph::get_module_id(&compilation.module_ids_artifact, module.module_identifier)
48 })
49 {
50 module_id.hash(hasher);
51 }
52
53 if let Some(runtime_chunk) = compilation
54 .build_chunk_graph_artifact
55 .chunk_group_by_ukey
56 .get(entry)
57 .map(|e| e.get_runtime_chunk(&compilation.build_chunk_graph_artifact.chunk_group_by_ukey))
58 {
59 for chunk_ukey in get_all_chunks(
60 entry,
61 chunk,
62 Some(&runtime_chunk),
63 &compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
64 ) {
65 if let Some(chunk) = compilation
66 .build_chunk_graph_artifact
67 .chunk_by_ukey
68 .get(&chunk_ukey)
69 {
70 chunk.id().hash(hasher);
71 }
72 }
73 }
74 }
75}
76
77pub fn get_all_chunks(
78 entrypoint: &ChunkGroupUkey,
79 exclude_chunk1: &ChunkUkey,
80 exclude_chunk2: Option<&ChunkUkey>,
81 chunk_group_by_ukey: &ChunkGroupByUkey,
82) -> FxIndexSet<ChunkUkey> {
83 fn add_chunks(
84 chunk_group_by_ukey: &ChunkGroupByUkey,
85 chunks: &mut FxIndexSet<ChunkUkey>,
86 entrypoint_ukey: &ChunkGroupUkey,
87 exclude_chunk1: &ChunkUkey,
88 exclude_chunk2: Option<&ChunkUkey>,
89 visit_chunk_groups: &mut FxIndexSet<ChunkGroupUkey>,
90 ) {
91 if let Some(entrypoint) = chunk_group_by_ukey.get(entrypoint_ukey) {
92 for chunk in &entrypoint.chunks {
93 if chunk == exclude_chunk1 {
94 continue;
95 }
96 if let Some(exclude_chunk2) = exclude_chunk2
97 && chunk == exclude_chunk2
98 {
99 continue;
100 }
101 chunks.insert(*chunk);
102 }
103
104 for parent in entrypoint.parents_iterable() {
105 if visit_chunk_groups.contains(parent) {
106 continue;
107 }
108 visit_chunk_groups.insert(*parent);
109 if let Some(chunk_group) = chunk_group_by_ukey.get(parent)
110 && chunk_group.is_initial()
111 {
112 add_chunks(
113 chunk_group_by_ukey,
114 chunks,
115 &chunk_group.ukey,
116 exclude_chunk1,
117 exclude_chunk2,
118 visit_chunk_groups,
119 );
120 }
121 }
122 }
123 }
124
125 let mut chunks = FxIndexSet::default();
126 let mut visit_chunk_groups = FxIndexSet::default();
127
128 add_chunks(
129 chunk_group_by_ukey,
130 &mut chunks,
131 entrypoint,
132 exclude_chunk1,
133 exclude_chunk2,
134 &mut visit_chunk_groups,
135 );
136
137 chunks
138}
139
140pub async fn get_runtime_chunk_output_name(
141 compilation: &Compilation,
142 chunk_ukey: &ChunkUkey,
143) -> Result<String> {
144 let entry_point = {
145 let entry_points = compilation
146 .build_chunk_graph_artifact
147 .chunk_graph
148 .get_chunk_entry_modules_with_chunk_group_iterable(chunk_ukey);
149
150 let (_, entry_point_ukey) = entry_points
151 .iter()
152 .next()
153 .ok_or_else(|| error!("should has entry point ukey"))?;
154
155 compilation
156 .build_chunk_graph_artifact
157 .chunk_group_by_ukey
158 .expect_get(entry_point_ukey)
159 };
160
161 let runtime_chunk = compilation
162 .build_chunk_graph_artifact
163 .chunk_by_ukey
164 .expect_get(
165 &entry_point.get_runtime_chunk(&compilation.build_chunk_graph_artifact.chunk_group_by_ukey),
166 );
167
168 get_chunk_output_name(runtime_chunk, compilation).await
169}
170
171pub fn runtime_chunk_has_hash(compilation: &Compilation, chunk_ukey: &ChunkUkey) -> Result<bool> {
172 let entry_point = {
173 let entry_points = compilation
174 .build_chunk_graph_artifact
175 .chunk_graph
176 .get_chunk_entry_modules_with_chunk_group_iterable(chunk_ukey);
177
178 let (_, entry_point_ukey) = entry_points
179 .iter()
180 .next()
181 .ok_or_else(|| error!("should has entry point ukey"))?;
182
183 compilation
184 .build_chunk_graph_artifact
185 .chunk_group_by_ukey
186 .expect_get(entry_point_ukey)
187 };
188
189 let runtime_chunk_ukey =
190 entry_point.get_runtime_chunk(&compilation.build_chunk_graph_artifact.chunk_group_by_ukey);
191 let runtime_chunk = compilation
192 .build_chunk_graph_artifact
193 .chunk_by_ukey
194 .expect_get(&runtime_chunk_ukey);
195
196 let filename = get_js_chunk_filename_template(
197 runtime_chunk,
198 &compilation.options.output,
199 &compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
200 );
201
202 if filename.has_hash_placeholder() {
203 return Ok(true);
204 }
205
206 if filename.has_content_hash_placeholder()
207 && (compilation
208 .build_chunk_graph_artifact
209 .chunk_graph
210 .has_chunk_full_hash_modules(&runtime_chunk_ukey, &compilation.runtime_modules)
211 || compilation
212 .build_chunk_graph_artifact
213 .chunk_graph
214 .has_chunk_dependent_hash_modules(&runtime_chunk_ukey, &compilation.runtime_modules))
215 {
216 return Ok(true);
217 }
218
219 Ok(false)
220}
221
222pub fn generate_entry_startup(
223 compilation: &Compilation,
224 chunk: &ChunkUkey,
225 entries: &IdentifierLinkedMap<ChunkGroupUkey>,
226 passive: bool,
227 runtime_template: &RuntimeCodeTemplate<'_>,
228) -> BoxSource {
229 let mut module_id_exprs = vec![];
230 let mut chunks_ids = ChunkIdSet::default();
231 let module_graph = compilation.get_module_graph();
232 for (module, entry) in entries {
233 if let Some(module_id) = module_graph
234 .module_by_identifier(module)
235 .filter(|module| {
236 module
237 .source_types(module_graph)
238 .contains(&SourceType::JavaScript)
239 })
240 .and_then(|module| {
241 ChunkGraph::get_module_id(&compilation.module_ids_artifact, module.identifier())
242 })
243 {
244 let module_id_expr = rspack_util::json_stringify(module_id);
245 module_id_exprs.push(module_id_expr);
246 } else {
247 continue;
248 }
249
250 if let Some(runtime_chunk) = compilation
251 .build_chunk_graph_artifact
252 .chunk_group_by_ukey
253 .get(entry)
254 .map(|e| e.get_runtime_chunk(&compilation.build_chunk_graph_artifact.chunk_group_by_ukey))
255 {
256 let chunks = get_all_chunks(
257 entry,
258 chunk,
259 Some(&runtime_chunk),
260 &compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
261 );
262 chunks_ids.extend(
263 chunks
264 .iter()
265 .map(|chunk_ukey| {
266 let chunk = compilation
267 .build_chunk_graph_artifact
268 .chunk_by_ukey
269 .expect_get(chunk_ukey);
270 chunk.expect_id().clone()
271 })
272 .collect::<HashSet<_>>(),
273 );
274 }
275 }
276
277 let mut source = String::default();
278 source.push_str(&format!(
279 "var {} = function(moduleId) {{ return {}({} = moduleId) }}\n",
280 runtime_template.render_runtime_variable(&RuntimeVariable::StartupExec),
281 runtime_template.render_runtime_globals(&RuntimeGlobals::REQUIRE),
282 runtime_template.render_runtime_globals(&RuntimeGlobals::ENTRY_MODULE_ID)
283 ));
284
285 let exports_name = runtime_template.render_runtime_variable(&RuntimeVariable::Exports);
286
287 let module_ids_code = &module_id_exprs
288 .iter()
289 .map(|module_id_expr| {
290 format!(
291 "{}({module_id_expr})",
292 runtime_template.render_runtime_variable(&RuntimeVariable::StartupExec)
293 )
294 })
295 .collect::<Vec<_>>()
296 .join(", ");
297 if chunks_ids.is_empty() {
298 if !module_ids_code.is_empty() {
299 if passive {
300 source.push_str(&format!("var {exports_name} = ("));
301 source.push_str(module_ids_code);
302 source.push_str(");\n");
303 } else {
304 source.push_str(&format!(
306 "var {exports_name} = {}(0, [], function() {{\n return {};\n }});\n",
307 runtime_template.render_runtime_globals(&RuntimeGlobals::STARTUP_ENTRYPOINT),
308 module_ids_code
309 ));
310 }
311 }
312 } else {
313 if !passive {
314 source.push_str(&format!("var {exports_name} = "));
315 }
316 source.push_str(&format!(
317 "{}(0, {}, function() {{
318 return {};
319 }});\n",
320 if passive {
321 runtime_template.render_runtime_globals(&RuntimeGlobals::ON_CHUNKS_LOADED)
322 } else {
323 runtime_template.render_runtime_globals(&RuntimeGlobals::STARTUP_ENTRYPOINT)
324 },
325 stringify_chunks_to_array(&chunks_ids),
326 module_ids_code
327 ));
328 if passive {
329 source.push_str(&format!(
330 "var {exports_name} = {}();\n",
331 runtime_template.render_runtime_globals(&RuntimeGlobals::ON_CHUNKS_LOADED)
332 ));
333 }
334 }
335
336 RawStringSource::from(source).boxed()
337}
338
339pub fn get_relative_path(base_chunk_output_name: &str, other_chunk_output_name: &str) -> String {
343 let mut base_chunk_output_name_arr = base_chunk_output_name.split('/').collect::<Vec<_>>();
344 base_chunk_output_name_arr.pop();
345 let mut other_chunk_output_name_arr = other_chunk_output_name.split('/').collect::<Vec<_>>();
346 while !base_chunk_output_name_arr.is_empty()
347 && !other_chunk_output_name_arr.is_empty()
348 && base_chunk_output_name_arr[0] == other_chunk_output_name_arr[0]
349 {
350 base_chunk_output_name_arr.remove(0);
351 other_chunk_output_name_arr.remove(0);
352 }
353 let path = if base_chunk_output_name_arr.is_empty() {
354 "./".to_string()
355 } else {
356 "../".repeat(base_chunk_output_name_arr.len())
357 };
358 format!("{path}{}", other_chunk_output_name_arr.join("/"))
359}
360
361pub async fn get_chunk_output_name(chunk: &Chunk, compilation: &Compilation) -> Result<String> {
362 let hash = chunk.rendered_hash(
363 &compilation.chunk_hashes_artifact,
364 compilation.options.output.hash_digest_length,
365 );
366 let filename = get_js_chunk_filename_template(
367 chunk,
368 &compilation.options.output,
369 &compilation.build_chunk_graph_artifact.chunk_group_by_ukey,
370 );
371 compilation
372 .get_path(
373 &filename,
374 PathData::default()
375 .chunk_id_optional(chunk.id().map(|id| id.as_str()))
376 .chunk_hash_optional(chunk.rendered_hash(
377 &compilation.chunk_hashes_artifact,
378 compilation.options.output.hash_digest_length,
379 ))
380 .chunk_name_optional(chunk.name_for_filename_template())
381 .content_hash_optional(chunk.rendered_content_hash_by_source_type(
382 &compilation.chunk_hashes_artifact,
383 &SourceType::JavaScript,
384 compilation.options.output.hash_digest_length,
385 ))
386 .hash_optional(hash),
387 )
388 .await
389}
390
391pub fn get_chunk_runtime_requirements<'a>(
392 compilation: &'a Compilation,
393 chunk_ukey: &ChunkUkey,
394) -> &'a RuntimeGlobals {
395 ChunkGraph::get_chunk_runtime_requirements(compilation, chunk_ukey)
396}
397
398static EJS_RUNTIME_GLOBALS_RE: LazyLock<Regex> = LazyLock::new(|| {
400 Regex::new(r"<%\-\s*([A-Z][A-Z0-9_]*)\s*%>").expect("invalid EJS runtime globals regex")
401});
402
403pub fn extract_runtime_globals_from_ejs(ejs_content: &str) -> RuntimeGlobals {
409 let names = EJS_RUNTIME_GLOBALS_RE
410 .captures_iter(ejs_content)
411 .map(|cap| cap[1].to_string())
412 .filter(|name| name.as_str() != "SCRIPT_NONCE")
414 .collect_vec();
415 RuntimeGlobals::from_names(&names)
416}
417
418#[cfg(test)]
419mod tests {
420 use super::{RuntimeGlobals, extract_runtime_globals_from_ejs};
421
422 fn expected_globals(names: &[&str]) -> RuntimeGlobals {
423 let names: Vec<String> = names.iter().map(|s| (*s).to_string()).collect();
424 RuntimeGlobals::from_names(&names)
425 }
426
427 #[test]
428 fn test_extract_runtime_globals_empty() {
429 assert!(extract_runtime_globals_from_ejs("").is_empty());
430 assert!(extract_runtime_globals_from_ejs("plain text").is_empty());
431 }
432
433 #[test]
434 fn test_extract_runtime_globals_single() {
435 assert_eq!(
436 extract_runtime_globals_from_ejs("<%- PUBLIC_PATH %>"),
437 expected_globals(&["PUBLIC_PATH"])
438 );
439 assert_eq!(
440 extract_runtime_globals_from_ejs("var x = <%- REQUIRE %>;"),
441 expected_globals(&["REQUIRE"])
442 );
443 }
444
445 #[test]
446 fn test_extract_runtime_globals_multiple_unique() {
447 let ejs = r#"link.href = <%- PUBLIC_PATH %> + <%- GET_CHUNK_SCRIPT_FILENAME %>(chunkId);
448 if (<%- HAS_OWN_PROPERTY %>(installedChunks, chunkId)) {}"#;
449 assert_eq!(
450 extract_runtime_globals_from_ejs(ejs),
451 expected_globals(&[
452 "PUBLIC_PATH",
453 "GET_CHUNK_SCRIPT_FILENAME",
454 "HAS_OWN_PROPERTY"
455 ])
456 );
457 }
458
459 #[test]
460 fn test_extract_runtime_globals_deduplicate() {
461 let ejs = "<%- REQUIRE %>; <%- PUBLIC_PATH %>; <%- REQUIRE %>; <%- PUBLIC_PATH %>";
462 assert_eq!(
463 extract_runtime_globals_from_ejs(ejs),
464 expected_globals(&["REQUIRE", "PUBLIC_PATH"])
465 );
466 }
467
468 #[test]
469 fn test_extract_runtime_globals_with_spaces() {
470 assert_eq!(
471 extract_runtime_globals_from_ejs("<%- ENSURE_CHUNK %>"),
472 expected_globals(&["ENSURE_CHUNK"])
473 );
474 }
475
476 #[test]
477 fn test_extract_runtime_globals_ignore_non_globals() {
478 let ejs = r#"<%- basicFunction("resolve, reject") %>
480 <%- _modules %>
481 <%- _cross_origin %>
482 <%- MODULE_CACHE %>"#;
483 assert_eq!(
484 extract_runtime_globals_from_ejs(ejs),
485 expected_globals(&["MODULE_CACHE"])
486 );
487 }
488
489 #[test]
490 fn test_extract_runtime_globals_uppercase_with_underscores() {
491 let ejs = "<%- GET_CHUNK_UPDATE_SCRIPT_FILENAME %> <%- HMR_DOWNLOAD_UPDATE_HANDLERS %>";
492 assert_eq!(
493 extract_runtime_globals_from_ejs(ejs),
494 expected_globals(&[
495 "GET_CHUNK_UPDATE_SCRIPT_FILENAME",
496 "HMR_DOWNLOAD_UPDATE_HANDLERS"
497 ])
498 );
499 }
500
501 #[test]
502 fn test_extract_runtime_globals_real_ejs_snippet() {
503 let ejs = r#"var installChunk = <%- basicFunction("data") %> {
504 var <%- _modules %> = data.<%- _modules %>;
505 for (moduleId in <%- _modules %>) {
506 if (<%- HAS_OWN_PROPERTY %>(<%- _modules %>, moduleId)) {
507 <%- MODULE_FACTORIES %>[moduleId] = <%- _modules %>[moduleId];
508 }
509 }
510 if (__rspack_esm_runtime) __rspack_esm_runtime(<%- REQUIRE %>);
511};"#;
512 assert_eq!(
513 extract_runtime_globals_from_ejs(ejs),
514 expected_globals(&["HAS_OWN_PROPERTY", "MODULE_FACTORIES", "REQUIRE"])
515 );
516 }
517}