Skip to main content

turbopack_ecmascript/chunk_list/
merged_update.rs

1//! Shared wire-format types for merged ecmascript chunk updates.
2//!
3//! These serde structures are the protocol contract sent to the JS HMR client
4//! (see `applyEcmascriptMergedUpdateShared` in the ecmascript runtime). Both the
5//! browser and node chunking contexts serialize into exactly this shape, so the
6//! definitions live here to keep the two runtimes from drifting apart on the
7//! wire format.
8//!
9//! The turbo-tasks value types (`*MergedChunkContent`, `*MergedChunkVersion`,
10//! `*ChunkContentMerger`) cannot be generic and therefore remain per-runtime,
11//! but they all build and serialize these shared structs.
12
13use anyhow::Result;
14use serde::Serialize;
15use turbo_tasks::{FxIndexMap, FxIndexSet, Vc};
16use turbo_tasks_fs::rope::Rope;
17use turbopack_core::{chunk::ModuleId, code_builder::Code, source_map::GenerateSourceMap};
18
19/// A merged update covering one or more ecmascript chunks that share a merger.
20#[derive(Serialize, Default)]
21#[serde(
22    tag = "type",
23    rename = "EcmascriptMergedUpdate",
24    rename_all = "camelCase"
25)]
26pub struct EcmascriptMergedUpdate<'a> {
27    /// A map from module id to its latest module entry (code + source map url).
28    #[serde(skip_serializing_if = "FxIndexMap::is_empty")]
29    pub entries: FxIndexMap<ModuleId, EcmascriptModuleEntry>,
30    /// A map from chunk path to the update for that chunk.
31    #[serde(skip_serializing_if = "FxIndexMap::is_empty")]
32    pub chunks: FxIndexMap<&'a str, EcmascriptMergedChunkUpdate>,
33}
34
35impl EcmascriptMergedUpdate<'_> {
36    pub fn is_empty(&self) -> bool {
37        self.entries.is_empty() && self.chunks.is_empty()
38    }
39}
40
41/// Per-chunk portion of an [`EcmascriptMergedUpdate`].
42#[derive(Serialize)]
43#[serde(tag = "type", rename_all = "camelCase")]
44pub enum EcmascriptMergedChunkUpdate {
45    Added(EcmascriptMergedChunkAdded),
46    Deleted(EcmascriptMergedChunkDeleted),
47    Partial(EcmascriptMergedChunkPartial),
48}
49
50/// A chunk that was newly added in this version.
51#[derive(Serialize, Default)]
52#[serde(rename_all = "camelCase")]
53pub struct EcmascriptMergedChunkAdded {
54    #[serde(skip_serializing_if = "FxIndexSet::is_empty")]
55    pub modules: FxIndexSet<ModuleId>,
56}
57
58/// A chunk that was removed in this version.
59#[derive(Serialize, Default)]
60#[serde(rename_all = "camelCase")]
61pub struct EcmascriptMergedChunkDeleted {
62    // Technically, this is redundant, since the client will already know all
63    // modules in the chunk from the previous version. However, it's useful for
64    // merging updates without access to an initial state.
65    #[serde(skip_serializing_if = "FxIndexSet::is_empty")]
66    pub modules: FxIndexSet<ModuleId>,
67}
68
69/// A chunk that was present in both versions and whose module membership
70/// changed.
71#[derive(Serialize, Default)]
72#[serde(rename_all = "camelCase")]
73pub struct EcmascriptMergedChunkPartial {
74    #[serde(skip_serializing_if = "FxIndexSet::is_empty")]
75    pub added: FxIndexSet<ModuleId>,
76    #[serde(skip_serializing_if = "FxIndexSet::is_empty")]
77    pub deleted: FxIndexSet<ModuleId>,
78}
79
80/// The code (and source map) for a single module in a merged update.
81#[derive(Serialize)]
82pub struct EcmascriptModuleEntry {
83    #[serde(with = "turbo_tasks_fs::rope::ser_as_string")]
84    pub code: Rope,
85    pub url: String,
86    #[serde(with = "turbo_tasks_fs::rope::ser_option_as_string")]
87    pub map: Option<Rope>,
88}
89
90impl EcmascriptModuleEntry {
91    pub async fn from_code(id: &ModuleId, code: Vc<Code>, chunk_path: &str) -> Result<Self> {
92        let map = &*code.generate_source_map().await?;
93        let map = map.as_content().map(|f| f.content().clone());
94
95        /// serde_qs can't serialize a lone enum when it's [serde::untagged].
96        #[derive(Serialize)]
97        struct Id<'a> {
98            id: &'a ModuleId,
99        }
100        let id = serde_qs::to_string(&Id { id }).unwrap();
101
102        Ok(EcmascriptModuleEntry {
103            // Cloning a rope is cheap.
104            code: code.await?.source_code().clone(),
105            url: format!("{}?{}", chunk_path, id),
106            map,
107        })
108    }
109}