rspack_plugin_runtime/
module_chunk_format.rs

1use std::hash::Hash;
2
3use rspack_core::{
4  ChunkGraph, ChunkKind, ChunkUkey, Compilation, CompilationAdditionalChunkRuntimeRequirements,
5  CompilationDependentFullHash, CompilationParams, CompilerCompilation, ModuleIdentifier, Plugin,
6  RuntimeGlobals, SourceType,
7  rspack_sources::{ConcatSource, RawStringSource, Source, SourceExt},
8};
9use rspack_error::Result;
10use rspack_hash::RspackHash;
11use rspack_hook::{plugin, plugin_hook};
12use rspack_plugin_javascript::{
13  JavascriptModulesChunkHash, JavascriptModulesRenderChunk, JavascriptModulesRenderStartup,
14  JsPlugin, RenderSource, runtime::render_chunk_runtime_modules,
15};
16use rspack_util::{itoa, json_stringify};
17use rustc_hash::FxHashSet as HashSet;
18
19use super::update_hash_for_entry_startup;
20use crate::{
21  chunk_has_js, get_all_chunks, get_chunk_output_name, get_relative_path,
22  get_runtime_chunk_output_name, runtime_chunk_has_hash,
23};
24
25const PLUGIN_NAME: &str = "rspack.ModuleChunkFormatPlugin";
26
27#[plugin]
28#[derive(Debug, Default)]
29pub struct ModuleChunkFormatPlugin;
30
31#[plugin_hook(CompilerCompilation for ModuleChunkFormatPlugin)]
32async fn compilation(
33  &self,
34  compilation: &mut Compilation,
35  _params: &mut CompilationParams,
36) -> Result<()> {
37  let hooks = JsPlugin::get_compilation_hooks_mut(compilation.id());
38  let mut hooks = hooks.write().await;
39  hooks.render_chunk.tap(render_chunk::new(self));
40  hooks.render_startup.tap(render_startup::new(self));
41  hooks.chunk_hash.tap(js_chunk_hash::new(self));
42  Ok(())
43}
44
45#[plugin_hook(CompilationAdditionalChunkRuntimeRequirements for ModuleChunkFormatPlugin)]
46async fn additional_chunk_runtime_requirements(
47  &self,
48  compilation: &mut Compilation,
49  chunk_ukey: &ChunkUkey,
50  runtime_requirements: &mut RuntimeGlobals,
51) -> Result<()> {
52  let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey);
53
54  if chunk.has_runtime(&compilation.chunk_group_by_ukey) {
55    return Ok(());
56  }
57
58  if compilation
59    .chunk_graph
60    .get_number_of_entry_modules(chunk_ukey)
61    > 0
62  {
63    runtime_requirements.insert(RuntimeGlobals::REQUIRE);
64    runtime_requirements.insert(RuntimeGlobals::STARTUP_ENTRYPOINT);
65    runtime_requirements.insert(RuntimeGlobals::EXTERNAL_INSTALL_CHUNK);
66  }
67
68  Ok(())
69}
70
71#[plugin_hook(JavascriptModulesChunkHash for ModuleChunkFormatPlugin)]
72async fn js_chunk_hash(
73  &self,
74  compilation: &Compilation,
75  chunk_ukey: &ChunkUkey,
76  hasher: &mut RspackHash,
77) -> Result<()> {
78  let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey);
79  if chunk.has_runtime(&compilation.chunk_group_by_ukey) {
80    return Ok(());
81  }
82
83  PLUGIN_NAME.hash(hasher);
84
85  update_hash_for_entry_startup(
86    hasher,
87    compilation,
88    compilation
89      .chunk_graph
90      .get_chunk_entry_modules_with_chunk_group_iterable(chunk_ukey),
91    chunk_ukey,
92  );
93
94  Ok(())
95}
96
97#[plugin_hook(CompilationDependentFullHash for ModuleChunkFormatPlugin)]
98async fn compilation_dependent_full_hash(
99  &self,
100  compilation: &Compilation,
101  chunk_ukey: &ChunkUkey,
102) -> Result<Option<bool>> {
103  if !chunk_has_js(chunk_ukey, compilation) {
104    return Ok(None);
105  }
106
107  let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey);
108
109  if !chunk.has_entry_module(&compilation.chunk_graph) {
110    return Ok(None);
111  }
112
113  if runtime_chunk_has_hash(compilation, chunk_ukey).await? {
114    return Ok(Some(true));
115  }
116
117  Ok(None)
118}
119
120#[plugin_hook(JavascriptModulesRenderChunk for ModuleChunkFormatPlugin)]
121async fn render_chunk(
122  &self,
123  compilation: &Compilation,
124  chunk_ukey: &ChunkUkey,
125  render_source: &mut RenderSource,
126) -> Result<()> {
127  let hooks = JsPlugin::get_compilation_hooks(compilation.id());
128  let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey);
129  let base_chunk_output_name = get_chunk_output_name(chunk, compilation).await?;
130
131  let chunk_id_json_string = json_stringify(chunk.expect_id(&compilation.chunk_ids_artifact));
132
133  let mut sources = ConcatSource::default();
134  sources.add(RawStringSource::from(format!(
135    "export const __webpack_id__ = {chunk_id_json_string} ;\n",
136  )));
137  sources.add(RawStringSource::from(format!(
138    "export const __webpack_ids__ = [{chunk_id_json_string}];\n",
139  )));
140  sources.add(RawStringSource::from_static(
141    "export const __webpack_modules__ = ",
142  ));
143  sources.add(render_source.source.clone());
144  sources.add(RawStringSource::from_static(";\n"));
145
146  if compilation
147    .chunk_graph
148    .has_chunk_runtime_modules(chunk_ukey)
149  {
150    sources.add(RawStringSource::from_static(
151      "export const __webpack_runtime__ = ",
152    ));
153    sources.add(render_chunk_runtime_modules(compilation, chunk_ukey).await?);
154    sources.add(RawStringSource::from_static(";\n"));
155  }
156
157  if matches!(chunk.kind(), ChunkKind::HotUpdate) {
158    render_source.source = sources.boxed();
159    return Ok(());
160  }
161
162  if chunk.has_entry_module(&compilation.chunk_graph) {
163    let runtime_chunk_output_name = get_runtime_chunk_output_name(compilation, chunk_ukey).await?;
164    sources.add(RawStringSource::from(format!(
165      "import __webpack_require__ from '{}';\n",
166      get_relative_path(
167        base_chunk_output_name
168          .trim_start_matches("/")
169          .trim_start_matches("\\"),
170        &runtime_chunk_output_name
171      )
172    )));
173
174    let entries = compilation
175      .chunk_graph
176      .get_chunk_entry_modules_with_chunk_group_iterable(chunk_ukey);
177
178    let mut startup_source = vec![];
179
180    startup_source.push(format!(
181      "var __webpack_exec__ = function(moduleId) {{ return __webpack_require__({} = moduleId); }}",
182      RuntimeGlobals::ENTRY_MODULE_ID
183    ));
184
185    let module_graph = compilation.get_module_graph();
186    let mut loaded_chunks = HashSet::default();
187    for (i, (module, entry)) in entries.iter().enumerate() {
188      if !module_graph
189        .module_by_identifier(module)
190        .is_some_and(|module| {
191          module
192            .source_types(&module_graph)
193            .contains(&SourceType::JavaScript)
194        })
195      {
196        continue;
197      }
198      let module_id = ChunkGraph::get_module_id(&compilation.module_ids_artifact, *module)
199        .expect("should have module id");
200      let runtime_chunk = compilation
201        .chunk_group_by_ukey
202        .expect_get(entry)
203        .get_runtime_chunk(&compilation.chunk_group_by_ukey);
204      let chunks = get_all_chunks(
205        entry,
206        &runtime_chunk,
207        None,
208        &compilation.chunk_group_by_ukey,
209      );
210
211      for chunk_ukey in chunks.iter() {
212        // Skip processing if the chunk doesn't have any JavaScript
213        if !chunk_has_js(chunk_ukey, compilation) {
214          continue;
215        }
216        if loaded_chunks.contains(chunk_ukey) {
217          continue;
218        }
219        loaded_chunks.insert(*chunk_ukey);
220        let index = loaded_chunks.len();
221        let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey);
222        let other_chunk_output_name = get_chunk_output_name(chunk, compilation).await?;
223        let mut index_buffer = itoa::Buffer::new();
224        let index_str = index_buffer.format(index);
225        startup_source.push(format!(
226          "import * as __webpack_chunk_${}__ from '{}';",
227          index_str,
228          get_relative_path(&base_chunk_output_name, &other_chunk_output_name)
229        ));
230        let mut index_buffer2 = itoa::Buffer::new();
231        let index_str2 = index_buffer2.format(index);
232        startup_source.push(format!(
233          "{}(__webpack_chunk_${}__);",
234          RuntimeGlobals::EXTERNAL_INSTALL_CHUNK,
235          index_str2
236        ));
237      }
238
239      let module_id_expr = serde_json::to_string(module_id).expect("invalid module_id");
240
241      startup_source.push(format!(
242        "{}__webpack_exec__({module_id_expr});",
243        if i + 1 == entries.len() {
244          "var __webpack_exports__ = "
245        } else {
246          ""
247        }
248      ));
249    }
250
251    let last_entry_module = entries
252      .keys()
253      .next_back()
254      .expect("should have last entry module");
255    let mut render_source = RenderSource {
256      source: RawStringSource::from(startup_source.join("\n")).boxed(),
257    };
258    hooks
259      .try_read()
260      .expect("should have js plugin drive")
261      .render_startup
262      .call(
263        compilation,
264        chunk_ukey,
265        last_entry_module,
266        &mut render_source,
267      )
268      .await?;
269    sources.add(render_source.source);
270  }
271  render_source.source = sources.boxed();
272  Ok(())
273}
274
275fn render_chunk_import(named_import: &str, import_source: &str) -> String {
276  format!("import * as {} from '{}';\n", named_import, import_source)
277}
278#[plugin_hook(JavascriptModulesRenderStartup for ModuleChunkFormatPlugin)]
279async fn render_startup(
280  &self,
281  compilation: &Compilation,
282  chunk_ukey: &ChunkUkey,
283  _module: &ModuleIdentifier,
284  render_source: &mut RenderSource,
285) -> Result<()> {
286  let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey);
287  let entries_count = compilation
288    .chunk_graph
289    .get_number_of_entry_modules(chunk_ukey);
290  let has_runtime = chunk.has_runtime(&compilation.chunk_group_by_ukey);
291
292  if entries_count > 0 && has_runtime {
293    let dependent_chunks = compilation
294      .chunk_graph
295      .get_chunk_entry_dependent_chunks_iterable(
296        chunk_ukey,
297        &compilation.chunk_by_ukey,
298        &compilation.chunk_group_by_ukey,
299      );
300    let base_chunk_output_name = get_chunk_output_name(chunk, compilation).await?;
301
302    let mut dependent_load = ConcatSource::default();
303    for (index, ck) in dependent_chunks.enumerate() {
304      if !chunk_has_js(&ck, compilation) {
305        continue;
306      }
307
308      let dependant_chunk = compilation.chunk_by_ukey.expect_get(&ck);
309
310      let named_import = format!("__webpack_imports__{}", index);
311
312      let dependant_chunk_name = get_chunk_output_name(dependant_chunk, compilation).await?;
313
314      let imported = get_relative_path(&base_chunk_output_name, &dependant_chunk_name);
315
316      dependent_load.add(RawStringSource::from(render_chunk_import(
317        &named_import,
318        &imported,
319      )));
320      dependent_load.add(RawStringSource::from(format!(
321        "{}({});\n",
322        RuntimeGlobals::EXTERNAL_INSTALL_CHUNK,
323        named_import
324      )));
325    }
326
327    if !dependent_load.source().is_empty() {
328      let mut sources = ConcatSource::default();
329      sources.add(dependent_load);
330      sources.add(render_source.source.clone());
331      render_source.source = sources.boxed();
332    }
333  }
334  Ok(())
335}
336
337impl Plugin for ModuleChunkFormatPlugin {
338  fn name(&self) -> &'static str {
339    PLUGIN_NAME
340  }
341
342  fn apply(&self, ctx: &mut rspack_core::ApplyContext<'_>) -> Result<()> {
343    ctx.compiler_hooks.compilation.tap(compilation::new(self));
344    ctx
345      .compilation_hooks
346      .additional_chunk_runtime_requirements
347      .tap(additional_chunk_runtime_requirements::new(self));
348    ctx
349      .compilation_hooks
350      .dependent_full_hash
351      .tap(compilation_dependent_full_hash::new(self));
352    Ok(())
353  }
354}