1use std::sync::Arc;
7
8use serde::{Deserialize, Serialize};
9
10use crate::BundleOutput;
11use crate::builders::asset_registry::{AssetInfo, AssetRegistry};
12use fob_graph::{CacheAnalysis, TransformationTrace};
13
14pub const CACHE_FORMAT_VERSION: u32 = 1;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct CachedBuild {
20 pub metadata: CacheMetadata,
22
23 pub outputs: Vec<SerializedOutput>,
25
26 pub graph_json: String,
28
29 pub entry_points: Vec<String>,
31
32 pub warnings: Vec<String>,
34
35 pub errors: Vec<String>,
37
38 pub cache: CacheAnalysis,
40
41 pub trace: Option<TransformationTrace>,
43
44 pub assets: Vec<SerializedAssetInfo>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct CacheMetadata {
51 pub fob_version: String,
53
54 pub format_version: u32,
56
57 pub created_at: u64,
59}
60
61impl CacheMetadata {
62 pub fn new() -> Self {
64 Self {
65 fob_version: env!("CARGO_PKG_VERSION").to_string(),
66 format_version: CACHE_FORMAT_VERSION,
67 created_at: std::time::SystemTime::now()
68 .duration_since(std::time::UNIX_EPOCH)
69 .map(|d| d.as_secs())
70 .unwrap_or(0),
71 }
72 }
73
74 pub fn is_compatible(&self) -> bool {
76 self.format_version == CACHE_FORMAT_VERSION
77 }
78}
79
80impl Default for CacheMetadata {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub enum SerializedOutput {
89 Chunk(SerializedChunk),
90 Asset(SerializedAsset),
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct SerializedChunk {
96 pub name: String,
97 pub filename: String,
98 pub code: String,
99 pub map_json: Option<String>,
100 pub sourcemap_filename: Option<String>,
101 pub preliminary_filename: String,
102 pub is_entry: bool,
103 pub is_dynamic_entry: bool,
104 pub facade_module_id: Option<String>,
105 pub module_ids: Vec<String>,
106 pub imports: Vec<String>,
107 pub dynamic_imports: Vec<String>,
108 pub exports: Vec<String>,
109 pub modules: Vec<SerializedRenderedModule>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct SerializedRenderedModule {
115 pub id: String,
116 pub code: Option<String>,
117 pub rendered_exports: Vec<String>,
118 pub exec_order: u32,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct SerializedAsset {
124 pub names: Vec<String>,
125 pub original_file_names: Vec<String>,
126 pub filename: String,
127 pub source: SerializedStrOrBytes,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub enum SerializedStrOrBytes {
133 Str(String),
134 Bytes(Vec<u8>),
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct SerializedAssetInfo {
140 pub source_path: String,
141 pub referrer: String,
142 pub specifier: String,
143 pub content_type: String,
144 pub size: Option<u64>,
145 pub url_path: Option<String>,
146 pub content_hash: Option<String>,
147}
148
149impl SerializedChunk {
154 pub fn from_rolldown(chunk: &rolldown_common::OutputChunk) -> Self {
156 Self {
157 name: chunk.name.to_string(),
158 filename: chunk.filename.to_string(),
159 code: chunk.code.clone(),
160 map_json: chunk.map.as_ref().map(|m| m.to_json_string()),
161 sourcemap_filename: chunk.sourcemap_filename.clone(),
162 preliminary_filename: chunk.preliminary_filename.clone(),
163 is_entry: chunk.is_entry,
164 is_dynamic_entry: chunk.is_dynamic_entry,
165 facade_module_id: chunk.facade_module_id.as_ref().map(|id| id.to_string()),
166 module_ids: chunk.module_ids.iter().map(|id| id.to_string()).collect(),
167 imports: chunk.imports.iter().map(|s| s.to_string()).collect(),
168 dynamic_imports: chunk
169 .dynamic_imports
170 .iter()
171 .map(|s| s.to_string())
172 .collect(),
173 exports: chunk.exports.iter().map(|s| s.to_string()).collect(),
174 modules: chunk
175 .modules
176 .keys
177 .iter()
178 .zip(chunk.modules.values.iter())
179 .map(|(id, module)| SerializedRenderedModule {
180 id: id.to_string(),
181 code: module.code(),
182 rendered_exports: module
183 .rendered_exports
184 .iter()
185 .map(|s| s.to_string())
186 .collect(),
187 exec_order: module.exec_order,
188 })
189 .collect(),
190 }
191 }
192}
193
194impl SerializedAsset {
195 pub fn from_rolldown(asset: &rolldown_common::OutputAsset) -> Self {
197 Self {
198 names: asset.names.clone(),
199 original_file_names: asset.original_file_names.clone(),
200 filename: asset.filename.to_string(),
201 source: match &asset.source {
202 rolldown_common::StrOrBytes::Str(s) => SerializedStrOrBytes::Str(s.clone()),
203 rolldown_common::StrOrBytes::Bytes(b) => SerializedStrOrBytes::Bytes(b.clone()),
204 },
205 }
206 }
207}
208
209impl SerializedOutput {
210 pub fn from_rolldown(output: &rolldown_common::Output) -> Self {
212 match output {
213 rolldown_common::Output::Chunk(chunk) => {
214 SerializedOutput::Chunk(SerializedChunk::from_rolldown(chunk))
215 }
216 rolldown_common::Output::Asset(asset) => {
217 SerializedOutput::Asset(SerializedAsset::from_rolldown(asset))
218 }
219 }
220 }
221}
222
223impl SerializedAssetInfo {
224 pub fn from_asset_info(info: &AssetInfo) -> Self {
226 Self {
227 source_path: info.source_path.to_string_lossy().to_string(),
228 referrer: info.referrer.clone(),
229 specifier: info.specifier.clone(),
230 content_type: info.content_type.clone(),
231 size: info.size,
232 url_path: info.url_path.clone(),
233 content_hash: info.content_hash.clone(),
234 }
235 }
236}
237
238impl SerializedChunk {
243 pub fn into_rolldown(self) -> rolldown_common::OutputChunk {
245 use arcstr::ArcStr;
246
247 let mut keys = Vec::with_capacity(self.modules.len());
249 let mut values = Vec::with_capacity(self.modules.len());
250
251 for module in self.modules {
252 keys.push(rolldown_common::ModuleId::new(module.id));
253 let rendered_exports = module
255 .rendered_exports
256 .into_iter()
257 .map(|s| s.into())
258 .collect();
259 values.push(Arc::new(rolldown_common::RenderedModule::new(
260 None, rendered_exports,
262 module.exec_order,
263 )));
264 }
265
266 rolldown_common::OutputChunk {
267 name: ArcStr::from(self.name),
268 filename: ArcStr::from(self.filename),
269 code: self.code,
270 map: None,
273 sourcemap_filename: self.sourcemap_filename,
274 preliminary_filename: self.preliminary_filename,
275 is_entry: self.is_entry,
276 is_dynamic_entry: self.is_dynamic_entry,
277 facade_module_id: self.facade_module_id.map(rolldown_common::ModuleId::new),
278 module_ids: self
279 .module_ids
280 .into_iter()
281 .map(rolldown_common::ModuleId::new)
282 .collect(),
283 imports: self.imports.into_iter().map(ArcStr::from).collect(),
284 dynamic_imports: self.dynamic_imports.into_iter().map(ArcStr::from).collect(),
285 exports: self.exports.into_iter().map(|s| s.into()).collect(),
286 modules: rolldown_common::Modules { keys, values },
287 }
288 }
289}
290
291impl SerializedAsset {
292 pub fn into_rolldown(self) -> rolldown_common::OutputAsset {
294 use arcstr::ArcStr;
295
296 rolldown_common::OutputAsset {
297 names: self.names,
298 original_file_names: self.original_file_names,
299 filename: ArcStr::from(self.filename),
300 source: match self.source {
301 SerializedStrOrBytes::Str(s) => rolldown_common::StrOrBytes::Str(s),
302 SerializedStrOrBytes::Bytes(b) => rolldown_common::StrOrBytes::Bytes(b),
303 },
304 }
305 }
306}
307
308impl SerializedOutput {
309 pub fn into_rolldown(self) -> rolldown_common::Output {
311 match self {
312 SerializedOutput::Chunk(chunk) => {
313 rolldown_common::Output::Chunk(Arc::new(chunk.into_rolldown()))
314 }
315 SerializedOutput::Asset(asset) => {
316 rolldown_common::Output::Asset(Arc::new(asset.into_rolldown()))
317 }
318 }
319 }
320}
321
322pub struct BuildComponents<'a> {
328 pub bundle: &'a BundleOutput,
329 pub graph_json: String,
330 pub entry_points: Vec<String>,
331 pub warnings: Vec<String>,
332 pub errors: Vec<String>,
333 pub cache: &'a CacheAnalysis,
334 pub trace: Option<&'a TransformationTrace>,
335 pub asset_registry: Option<&'a Arc<AssetRegistry>>,
336}
337
338impl CachedBuild {
339 pub fn from_components(components: BuildComponents<'_>) -> Self {
341 let outputs: Vec<SerializedOutput> = components
343 .bundle
344 .assets
345 .iter()
346 .map(SerializedOutput::from_rolldown)
347 .collect();
348
349 let assets = components
351 .asset_registry
352 .map(|registry| {
353 registry
354 .all_assets()
355 .iter()
356 .map(SerializedAssetInfo::from_asset_info)
357 .collect()
358 })
359 .unwrap_or_default();
360
361 Self {
362 metadata: CacheMetadata::new(),
363 outputs,
364 graph_json: components.graph_json,
365 entry_points: components.entry_points,
366 warnings: components.warnings,
367 errors: components.errors,
368 cache: components.cache.clone(),
369 trace: components.trace.cloned(),
370 assets,
371 }
372 }
373
374 pub fn into_bundle_output(self) -> BundleOutput {
376 BundleOutput {
377 assets: self
378 .outputs
379 .into_iter()
380 .map(|o| o.into_rolldown())
381 .collect(),
382 warnings: vec![], }
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn test_cache_metadata_compatibility() {
393 let meta = CacheMetadata::new();
394 assert!(meta.is_compatible());
395 assert_eq!(meta.format_version, CACHE_FORMAT_VERSION);
396 }
397
398 #[test]
399 fn test_serialized_str_or_bytes_roundtrip() {
400 let str_val = SerializedStrOrBytes::Str("hello".to_string());
401 let bytes_val = SerializedStrOrBytes::Bytes(vec![1, 2, 3]);
402
403 let str_bytes = bincode::serialize(&str_val).unwrap();
405 let str_back: SerializedStrOrBytes = bincode::deserialize(&str_bytes).unwrap();
406 assert!(matches!(str_back, SerializedStrOrBytes::Str(s) if s == "hello"));
407
408 let bytes_bytes = bincode::serialize(&bytes_val).unwrap();
409 let bytes_back: SerializedStrOrBytes = bincode::deserialize(&bytes_bytes).unwrap();
410 assert!(matches!(bytes_back, SerializedStrOrBytes::Bytes(b) if b == vec![1, 2, 3]));
411 }
412}