1use blake3::Hasher;
7use std::path::Path;
8
9use super::{CacheConfig, CacheResult};
10use crate::builders::common::BundlePlan;
11
12const CACHE_FORMAT_VERSION: u32 = 1;
14
15const ROLLDOWN_VERSION: &str = env!("CARGO_PKG_VERSION");
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub struct CacheKey(String);
22
23impl CacheKey {
24 pub fn from_hex(hex: impl Into<String>) -> Self {
26 Self(hex.into())
27 }
28
29 pub fn as_hex(&self) -> &str {
31 &self.0
32 }
33}
34
35impl std::fmt::Display for CacheKey {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 write!(f, "{}", self.0)
38 }
39}
40
41pub fn compute_cache_key(plan: &BundlePlan, config: &CacheConfig) -> CacheResult<CacheKey> {
51 let mut hasher = Hasher::new();
52
53 hasher.update(&CACHE_FORMAT_VERSION.to_le_bytes());
55
56 hasher.update(ROLLDOWN_VERSION.as_bytes());
58
59 hash_entries(&mut hasher, plan)?;
61
62 hash_bundler_options(&mut hasher, &plan.options);
64
65 hash_virtual_files(&mut hasher, &plan.virtual_files);
67
68 hash_env_vars(&mut hasher, &config.env_vars);
70
71 let hash = hasher.finalize();
72 Ok(CacheKey(hash.to_hex().to_string()))
73}
74
75fn hash_entries(hasher: &mut Hasher, plan: &BundlePlan) -> CacheResult<()> {
77 let mut entries: Vec<&str> = plan.entries.iter().map(|e| e.import.as_str()).collect();
79 entries.sort();
80
81 for entry in entries {
82 hasher.update(entry.as_bytes());
84 hasher.update(b"\0"); if !entry.starts_with("virtual:") {
88 if let Ok(content) = read_file_content(entry, plan) {
89 let content_hash = blake3::hash(&content);
90 hasher.update(content_hash.as_bytes());
91 }
92 }
94 }
95
96 Ok(())
97}
98
99fn read_file_content(path: &str, plan: &BundlePlan) -> std::io::Result<Vec<u8>> {
101 if let Some(content) = plan.virtual_files.get(path) {
103 return Ok(content.as_bytes().to_vec());
104 }
105
106 let resolved_path = if Path::new(path).is_absolute() {
108 Path::new(path).to_path_buf()
109 } else if let Some(cwd) = &plan.cwd {
110 cwd.join(path)
111 } else {
112 Path::new(path).to_path_buf()
113 };
114
115 std::fs::read(&resolved_path)
116}
117
118fn hash_bundler_options(hasher: &mut Hasher, options: &rolldown::BundlerOptions) {
124 if let Some(format) = &options.format {
126 hasher.update(format!("{:?}", format).as_bytes());
127 }
128
129 if let Some(platform) = &options.platform {
131 hasher.update(format!("{:?}", platform).as_bytes());
132 }
133
134 if let Some(sourcemap) = &options.sourcemap {
136 hasher.update(format!("{:?}", sourcemap).as_bytes());
137 }
138
139 if let Some(minify) = &options.minify {
141 hasher.update(format!("{:?}", minify).as_bytes());
142 }
143
144 if let Some(external) = &options.external {
146 hasher.update(format!("{:?}", external).as_bytes());
147 }
148
149 if let Some(globals) = &options.globals {
151 hasher.update(format!("{:?}", globals).as_bytes());
152 }
153
154 if let Some(resolve) = &options.resolve {
156 if let Some(alias) = &resolve.alias {
158 let mut aliases: Vec<_> = alias.iter().collect();
159 aliases.sort_by_key(|(k, _)| k.clone());
160 for (key, values) in aliases {
161 hasher.update(key.as_bytes());
162 hasher.update(format!("{:?}", values).as_bytes());
163 }
164 }
165
166 if let Some(conditions) = &resolve.condition_names {
168 let mut conds: Vec<_> = conditions.iter().collect();
169 conds.sort();
170 for cond in conds {
171 hasher.update(cond.as_bytes());
172 }
173 }
174 }
175
176 if let Some(advanced_chunks) = &options.advanced_chunks {
178 hasher.update(format!("{:?}", advanced_chunks).as_bytes());
179 }
180
181 if let Some(transform) = &options.transform {
183 hasher.update(format!("{:?}", transform).as_bytes());
184 }
185}
186
187fn hash_virtual_files(hasher: &mut Hasher, virtual_files: &rustc_hash::FxHashMap<String, String>) {
189 let mut paths: Vec<_> = virtual_files.keys().collect();
191 paths.sort();
192
193 for path in paths {
194 let content = &virtual_files[path];
195
196 hasher.update(path.as_bytes());
198 hasher.update(b"\0");
199
200 let content_hash = blake3::hash(content.as_bytes());
202 hasher.update(content_hash.as_bytes());
203 }
204}
205
206fn hash_env_vars(hasher: &mut Hasher, env_vars: &[String]) {
208 let mut vars: Vec<_> = env_vars.iter().collect();
210 vars.sort();
211
212 for var in vars {
213 hasher.update(var.as_bytes());
214 hasher.update(b"=");
215
216 if let Ok(value) = std::env::var(var) {
218 hasher.update(value.as_bytes());
219 }
220 hasher.update(b"\0");
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 fn make_test_plan(entries: Vec<&str>, vfiles: Vec<(&str, &str)>) -> BundlePlan {
229 BundlePlan {
230 entries: entries
231 .into_iter()
232 .map(|e| crate::builders::common::EntrySpec {
233 name: None,
234 import: e.to_string(),
235 })
236 .collect(),
237 options: rolldown::BundlerOptions::default(),
238 plugins: vec![],
239 cwd: None,
240 virtual_files: vfiles
241 .into_iter()
242 .map(|(k, v)| (k.to_string(), v.to_string()))
243 .collect(),
244 runtime: None,
245 cache: None,
246 incremental: None,
247 }
248 }
249
250 #[test]
251 fn test_cache_key_deterministic() {
252 let plan1 = make_test_plan(
253 vec!["virtual:a.js", "virtual:b.js"],
254 vec![
255 ("virtual:a.js", "export const a = 1;"),
256 ("virtual:b.js", "export const b = 2;"),
257 ],
258 );
259 let plan2 = make_test_plan(
260 vec!["virtual:b.js", "virtual:a.js"],
261 vec![
262 ("virtual:b.js", "export const b = 2;"),
263 ("virtual:a.js", "export const a = 1;"),
264 ],
265 );
266
267 let config = CacheConfig::default();
268
269 let key1 = compute_cache_key(&plan1, &config).unwrap();
270 let key2 = compute_cache_key(&plan2, &config).unwrap();
271
272 assert_eq!(key1, key2);
274 }
275
276 #[test]
277 fn test_cache_key_changes_on_content_change() {
278 let plan1 = make_test_plan(
279 vec!["virtual:a.js"],
280 vec![("virtual:a.js", "export const a = 1;")],
281 );
282 let plan2 = make_test_plan(
283 vec!["virtual:a.js"],
284 vec![("virtual:a.js", "export const a = 2;")],
285 );
286
287 let config = CacheConfig::default();
288
289 let key1 = compute_cache_key(&plan1, &config).unwrap();
290 let key2 = compute_cache_key(&plan2, &config).unwrap();
291
292 assert_ne!(key1, key2);
294 }
295
296 #[test]
297 fn test_cache_key_changes_on_entry_change() {
298 let plan1 = make_test_plan(
299 vec!["virtual:a.js"],
300 vec![("virtual:a.js", "export const a = 1;")],
301 );
302 let plan2 = make_test_plan(
303 vec!["virtual:b.js"],
304 vec![("virtual:b.js", "export const a = 1;")],
305 );
306
307 let config = CacheConfig::default();
308
309 let key1 = compute_cache_key(&plan1, &config).unwrap();
310 let key2 = compute_cache_key(&plan2, &config).unwrap();
311
312 assert_ne!(key1, key2);
314 }
315}