turbopack_ecmascript/hmr/
update.rs1use anyhow::Result;
2use turbo_frozenmap::{FrozenMap, FrozenSet};
3use turbo_tasks::{FxIndexMap, FxIndexSet, ReadRef, ResolvedVc, TryJoinIterExt, Vc};
4use turbopack_core::{
5 chunk::ModuleId,
6 code_builder::Code,
7 update_instruction::UpdateInstruction,
8 version::{PartialUpdate, TotalUpdate, Update, Version},
9};
10
11use crate::{
12 chunk::EcmascriptChunkContentEntries,
13 chunk_list::{
14 merged_update::{
15 EcmascriptMergedChunkAdded, EcmascriptMergedChunkDeleted, EcmascriptMergedChunkPartial,
16 EcmascriptMergedChunkUpdate, EcmascriptMergedUpdate, EcmascriptModuleEntry,
17 },
18 update::EcmascriptUpdateInstruction,
19 },
20 hmr::{
21 EcmascriptHmrChunkContent,
22 content::EcmascriptMergedChunkContent,
23 version::{EcmascriptChunkVersion, EcmascriptMergedChunkVersion},
24 },
25};
26
27enum EcmascriptChunkUpdate {
29 None,
30 Partial {
31 added: FxIndexMap<ModuleId, AddedModule>,
32 modified: FxIndexMap<ModuleId, ResolvedVc<Code>>,
33 deleted: FxIndexMap<ModuleId, u128>,
34 },
35}
36
37struct AddedModule {
38 hash: u128,
39 code: ResolvedVc<Code>,
40}
41
42async fn update_ecmascript_hmr_chunk_content(
45 content: Vc<Box<dyn EcmascriptHmrChunkContent>>,
46 to: &ReadRef<EcmascriptChunkVersion>,
47 from: &ReadRef<EcmascriptChunkVersion>,
48) -> Result<EcmascriptChunkUpdate> {
49 let mut added = FxIndexMap::default();
50 let mut modified = FxIndexMap::default();
51 let mut deleted = FxIndexMap::default();
52
53 let mut entries_ref = None;
58
59 for (id, from_hash) in &from.entries_hashes {
61 if let Some(to_hash) = to.entries_hashes.get(id) {
62 if *to_hash != *from_hash {
63 let entries = match &entries_ref {
65 Some(entries) => entries,
66 None => entries_ref.insert(content.entries().await?),
67 };
68 if let Some(entry) = entries.get(id) {
69 modified.insert(id.clone(), entry.code);
70 }
71 }
72 } else {
73 deleted.insert(id.clone(), *from_hash);
75 }
76 }
77
78 for (id, hash) in &to.entries_hashes {
80 if !from.entries_hashes.contains_key(id) {
81 let entries = match &entries_ref {
82 Some(entries) => entries,
83 None => entries_ref.insert(content.entries().await?),
84 };
85 if let Some(entry) = entries.get(id) {
86 added.insert(
87 id.clone(),
88 AddedModule {
89 hash: *hash,
90 code: entry.code,
91 },
92 );
93 }
94 }
95 }
96
97 Ok(
98 if added.is_empty() && modified.is_empty() && deleted.is_empty() {
99 EcmascriptChunkUpdate::None
100 } else {
101 EcmascriptChunkUpdate::Partial {
102 added,
103 modified,
104 deleted,
105 }
106 },
107 )
108}
109
110fn module_hash(versions: &[ReadRef<EcmascriptChunkVersion>], id: &ModuleId) -> Option<u128> {
113 versions
114 .iter()
115 .find_map(|version| version.entries_hashes.get(id).copied())
116}
117
118async fn insert_entry_unless_shipped(
121 entries: &mut FxIndexMap<ModuleId, EcmascriptModuleEntry>,
122 from_versions: &[ReadRef<EcmascriptChunkVersion>],
123 id: ModuleId,
124 hash: u128,
125 code: Vc<Code>,
126 chunk_path: &str,
127) -> Result<()> {
128 if module_hash(from_versions, &id) != Some(hash) {
129 let entry = EcmascriptModuleEntry::from_code(&id, code, chunk_path).await?;
130 entries.insert(id, entry);
131 }
132 Ok(())
133}
134
135async fn partial_chunk_update(
137 update: EcmascriptChunkUpdate,
138 chunk_path: &str,
139 from_versions: &[ReadRef<EcmascriptChunkVersion>],
140 entries: &mut FxIndexMap<ModuleId, EcmascriptModuleEntry>,
141) -> Result<EcmascriptMergedChunkUpdate> {
142 let EcmascriptChunkUpdate::Partial {
143 added,
144 modified,
145 deleted,
146 } = update
147 else {
148 unreachable!("caller filters out EcmascriptChunkUpdate::None");
149 };
150
151 let mut added_modules = FxIndexSet::default();
152
153 for (id, AddedModule { hash, code }) in added {
154 added_modules.insert(id.clone());
155 insert_entry_unless_shipped(entries, from_versions, id, hash, *code, chunk_path).await?;
156 }
157
158 for (id, code) in modified {
159 let entry = EcmascriptModuleEntry::from_code(&id, *code, chunk_path).await?;
160 entries.insert(id, entry);
161 }
162
163 Ok(EcmascriptMergedChunkUpdate::Partial(
164 EcmascriptMergedChunkPartial {
165 added: FrozenSet::from(added_modules),
166 deleted: deleted.into_keys().collect(),
167 },
168 ))
169}
170
171async fn added_chunk_update(
173 chunk_entries: &ReadRef<EcmascriptChunkContentEntries>,
174 chunk_path: &str,
175 from_versions: &[ReadRef<EcmascriptChunkVersion>],
176 entries: &mut FxIndexMap<ModuleId, EcmascriptModuleEntry>,
177) -> Result<EcmascriptMergedChunkUpdate> {
178 let mut modules = FxIndexSet::default();
179
180 for (id, entry) in chunk_entries.iter() {
181 modules.insert(id.clone());
182 insert_entry_unless_shipped(
183 entries,
184 from_versions,
185 id.clone(),
186 *entry.hash.await?,
187 *entry.code,
188 chunk_path,
189 )
190 .await?;
191 }
192
193 Ok(EcmascriptMergedChunkUpdate::Added(
194 EcmascriptMergedChunkAdded {
195 modules: FrozenSet::from(modules),
196 },
197 ))
198}
199
200pub async fn update_ecmascript_merged_chunk(
205 content: Vc<EcmascriptMergedChunkContent>,
206 from_version: ResolvedVc<Box<dyn Version>>,
207) -> Result<Update> {
208 let to_merged_version = content.version();
209 let Some(from_merged_version) =
210 ResolvedVc::try_downcast_type::<EcmascriptMergedChunkVersion>(from_version)
211 else {
212 return Ok(Update::Total(TotalUpdate {
214 to: Vc::upcast::<Box<dyn Version>>(to_merged_version)
215 .into_trait_ref()
216 .await?,
217 }));
218 };
219
220 let to = to_merged_version.await?;
221 let from = from_merged_version.await?;
222
223 if from.ptr_eq(&to) {
225 return Ok(Update::None);
226 }
227
228 let mut from_versions_by_chunk_path: FxIndexMap<_, _> = from
229 .versions
230 .iter()
231 .map(|version| (&*version.chunk_path, version))
232 .collect();
233
234 let from_versions = &from.versions;
235
236 let content = content.await?;
237 let to_contents = content
238 .contents
239 .iter()
240 .map(async |content| {
241 let entries = content.entries().await?;
242 let version = content.ecmascript_chunk_version().await?;
243 Ok((*content, entries, version))
244 })
245 .try_join()
246 .await?;
247
248 let mut merged_entries = FxIndexMap::default();
249 let mut chunks = FxIndexMap::default();
250
251 for (content, entries, to_version) in &to_contents {
252 let chunk_path = to_version.chunk_path.as_str();
253
254 let chunk_update = match from_versions_by_chunk_path.swap_remove(chunk_path) {
255 Some(from_version) => {
256 match update_ecmascript_hmr_chunk_content(**content, to_version, from_version)
257 .await?
258 {
259 EcmascriptChunkUpdate::None => continue,
260 update => {
261 partial_chunk_update(update, chunk_path, from_versions, &mut merged_entries)
262 .await?
263 }
264 }
265 }
266 None => {
267 added_chunk_update(entries, chunk_path, from_versions, &mut merged_entries).await?
268 }
269 };
270
271 chunks.insert(to_version.chunk_path.clone(), chunk_update);
272 }
273
274 for chunk_version in from_versions_by_chunk_path.into_values() {
275 let hashes = &chunk_version.entries_hashes;
276 chunks.insert(
277 chunk_version.chunk_path.clone(),
278 EcmascriptMergedChunkUpdate::Deleted(EcmascriptMergedChunkDeleted {
279 modules: hashes.keys().cloned().collect(),
280 }),
281 );
282 }
283
284 let merged_update = EcmascriptMergedUpdate {
285 entries: FrozenMap::from(merged_entries),
286 chunks: FrozenMap::from(chunks),
287 };
288
289 Ok(if merged_update.is_empty() {
290 Update::None
291 } else {
292 Update::Partial(PartialUpdate {
293 to: Vc::upcast::<Box<dyn Version>>(to_merged_version)
294 .into_trait_ref()
295 .await?,
296 instruction: UpdateInstruction::new(EcmascriptUpdateInstruction::Merged(merged_update)),
297 })
298 })
299}