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_frozenmap::{FrozenMap, FrozenSet};
16use turbo_rcstr::RcStr;
17use turbo_tasks::{NonLocalValue, Vc};
18use turbo_tasks_fs::rope::Rope;
19use turbopack_core::{chunk::ModuleId, code_builder::Code, source_map::GenerateSourceMap};
20
21/// A merged update covering one or more ecmascript chunks that share a merger.
22#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, NonLocalValue)]
23#[serde(
24    tag = "type",
25    rename = "EcmascriptMergedUpdate",
26    rename_all = "camelCase"
27)]
28pub struct EcmascriptMergedUpdate {
29    /// A map from module id to its latest module entry (code + source map url).
30    #[serde(skip_serializing_if = "FrozenMap::is_empty")]
31    pub entries: FrozenMap<ModuleId, EcmascriptModuleEntry>,
32    /// A map from chunk path to the update for that chunk.
33    #[serde(skip_serializing_if = "FrozenMap::is_empty")]
34    pub chunks: FrozenMap<RcStr, EcmascriptMergedChunkUpdate>,
35}
36
37impl EcmascriptMergedUpdate {
38    pub fn is_empty(&self) -> bool {
39        self.entries.is_empty() && self.chunks.is_empty()
40    }
41}
42
43/// Per-chunk portion of an [`EcmascriptMergedUpdate`].
44#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, NonLocalValue)]
45#[serde(tag = "type", rename_all = "camelCase")]
46pub enum EcmascriptMergedChunkUpdate {
47    Added(EcmascriptMergedChunkAdded),
48    Deleted(EcmascriptMergedChunkDeleted),
49    Partial(EcmascriptMergedChunkPartial),
50}
51
52/// A chunk that was newly added in this version.
53#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, NonLocalValue)]
54#[serde(rename_all = "camelCase")]
55pub struct EcmascriptMergedChunkAdded {
56    #[serde(skip_serializing_if = "FrozenSet::is_empty")]
57    pub modules: FrozenSet<ModuleId>,
58}
59
60/// A chunk that was removed in this version.
61#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, NonLocalValue)]
62#[serde(rename_all = "camelCase")]
63pub struct EcmascriptMergedChunkDeleted {
64    // Technically, this is redundant, since the client will already know all
65    // modules in the chunk from the previous version. However, it's useful for
66    // merging updates without access to an initial state.
67    #[serde(skip_serializing_if = "FrozenSet::is_empty")]
68    pub modules: FrozenSet<ModuleId>,
69}
70
71/// A chunk that was present in both versions and whose module membership
72/// changed.
73#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, NonLocalValue)]
74#[serde(rename_all = "camelCase")]
75pub struct EcmascriptMergedChunkPartial {
76    #[serde(skip_serializing_if = "FrozenSet::is_empty")]
77    pub added: FrozenSet<ModuleId>,
78    #[serde(skip_serializing_if = "FrozenSet::is_empty")]
79    pub deleted: FrozenSet<ModuleId>,
80}
81
82/// The code (and source map) for a single module in a merged update.
83#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, NonLocalValue)]
84pub struct EcmascriptModuleEntry {
85    #[serde(with = "turbo_tasks_fs::rope::ser_as_string")]
86    pub code: Rope,
87    pub url: RcStr,
88    #[serde(with = "turbo_tasks_fs::rope::ser_option_as_string")]
89    pub map: Option<Rope>,
90}
91
92impl EcmascriptModuleEntry {
93    pub async fn from_code(id: &ModuleId, code: Vc<Code>, chunk_path: &str) -> Result<Self> {
94        let map = &*code.generate_source_map().await?;
95        let map = map.as_content().map(|f| f.content().clone());
96
97        /// serde_qs can't serialize a lone enum when it's [serde::untagged].
98        #[derive(Serialize)]
99        struct Id<'a> {
100            id: &'a ModuleId,
101        }
102        let id = serde_qs::to_string(&Id { id }).unwrap();
103
104        Ok(EcmascriptModuleEntry {
105            // Cloning a rope is cheap.
106            code: code.await?.source_code().clone(),
107            url: format!("{}?{}", chunk_path, id).into(),
108            map,
109        })
110    }
111}