Skip to main content

turbopack_ecmascript/hmr/
version.rs

1use anyhow::Result;
2use turbo_rcstr::RcStr;
3use turbo_tasks::{FxIndexMap, ReadRef, TryJoinIterExt, Vc, turbobail};
4use turbo_tasks_fs::FileSystemPath;
5use turbo_tasks_hash::{Xxh3Hash64Hasher, encode_base64};
6use turbopack_core::{
7    chunk::{MinifyType, ModuleId},
8    version::Version,
9};
10
11use crate::chunk::{EcmascriptChunkContent, EcmascriptChunkContentEntries};
12
13/// The version of a single Ecmascript chunk's content, tracked as the set of
14/// per-module content hashes.
15///
16/// Runtime-agnostic: the browser and node chunking contexts share this one
17/// implementation rather than each carrying a copy. `minify_type` participates
18/// in the hash because minification changes the emitted bytes without changing
19/// any module's own hash.
20#[turbo_tasks::value(serialization = "skip")]
21pub struct EcmascriptChunkVersion {
22    pub chunk_path: RcStr,
23    pub minify_type: MinifyType,
24    pub entries_hashes: FxIndexMap<ModuleId, u128>,
25}
26
27#[turbo_tasks::value_impl]
28impl EcmascriptChunkVersion {
29    #[turbo_tasks::function]
30    pub async fn new(
31        output_root: FileSystemPath,
32        chunk_path: FileSystemPath,
33        content: Vc<EcmascriptChunkContent>,
34        minify_type: MinifyType,
35    ) -> Result<Vc<Self>> {
36        let Some(chunk_path) = output_root.get_path_to(&chunk_path) else {
37            turbobail!("chunk path {chunk_path} is not in output root {output_root}");
38        };
39        let entries_hashes = EcmascriptChunkContentEntries::new(content)
40            .await?
41            .iter()
42            .map(async |(id, entry)| Ok((id.clone(), *entry.hash.await?)))
43            .try_join()
44            .await?
45            .into_iter()
46            .collect();
47
48        Ok(EcmascriptChunkVersion {
49            chunk_path: chunk_path.into(),
50            minify_type,
51            entries_hashes,
52        }
53        .cell())
54    }
55}
56
57#[turbo_tasks::value_impl]
58impl Version for EcmascriptChunkVersion {
59    #[turbo_tasks::function]
60    fn id(&self) -> Vc<RcStr> {
61        let mut hasher = Xxh3Hash64Hasher::new();
62        hasher.write_ref(&self.chunk_path);
63        hasher.write_ref(&self.minify_type);
64        let sorted_hashes = {
65            let mut hashes: Vec<_> = self.entries_hashes.values().copied().collect();
66            hashes.sort();
67            hashes
68        };
69        for hash in sorted_hashes {
70            hasher.write_value(hash);
71        }
72        let hash = hasher.finish();
73        let hash = encode_base64(hash);
74        Vc::cell(hash.into())
75    }
76}
77
78/// The version of a [`super::content::EcmascriptMergedChunkContent`]. This is
79/// essentially a composite [`EcmascriptChunkVersion`].
80#[turbo_tasks::value(serialization = "skip", shared)]
81pub struct EcmascriptMergedChunkVersion {
82    #[turbo_tasks(trace_ignore)]
83    pub versions: Vec<ReadRef<EcmascriptChunkVersion>>,
84}
85
86#[turbo_tasks::value_impl]
87impl Version for EcmascriptMergedChunkVersion {
88    #[turbo_tasks::function]
89    async fn id(&self) -> Result<Vc<RcStr>> {
90        let mut hasher = Xxh3Hash64Hasher::new();
91        hasher.write_value(self.versions.len());
92        let sorted_ids = {
93            let mut sorted_ids = self
94                .versions
95                .iter()
96                // This `ReadRef::cell` call is important: it ensures the id is
97                // computed from a cell, so it is cached.
98                .map(|version| ReadRef::cell(version.clone()).id())
99                .try_join()
100                .await?;
101            sorted_ids.sort();
102            sorted_ids
103        };
104        for id in sorted_ids {
105            hasher.write_value(id);
106        }
107        let hash = hasher.finish();
108        let hash = encode_base64(hash);
109        Ok(Vc::cell(hash.into()))
110    }
111}