|
1 | 1 | use anyhow::{bail, Context, Result};
|
| 2 | +use serde::{Deserialize, Serialize}; |
2 | 3 | use tracing::Instrument;
|
3 | 4 | use turbo_rcstr::RcStr;
|
4 |
| -use turbo_tasks::{ResolvedVc, TryJoinIterExt, Upcast, Value, ValueToString, Vc}; |
| 5 | +use turbo_tasks::{ |
| 6 | + trace::TraceRawVcs, NonLocalValue, ResolvedVc, TaskInput, TryJoinIterExt, Upcast, Value, |
| 7 | + ValueToString, Vc, |
| 8 | +}; |
5 | 9 | use turbo_tasks_fs::FileSystemPath;
|
| 10 | +use turbo_tasks_hash::{hash_xxh3_hash64, DeterministicHash}; |
6 | 11 | use turbopack_core::{
|
7 |
| - asset::Asset, |
| 12 | + asset::{Asset, AssetContent}, |
8 | 13 | chunk::{
|
9 | 14 | availability_info::AvailabilityInfo,
|
10 | 15 | chunk_group::{make_chunk_group, MakeChunkGroupResult},
|
@@ -32,6 +37,31 @@ use crate::ecmascript::{
|
32 | 37 | list::asset::{EcmascriptDevChunkList, EcmascriptDevChunkListSource},
|
33 | 38 | };
|
34 | 39 |
|
| 40 | +#[derive( |
| 41 | + Debug, |
| 42 | + TaskInput, |
| 43 | + Clone, |
| 44 | + Copy, |
| 45 | + PartialEq, |
| 46 | + Eq, |
| 47 | + Hash, |
| 48 | + Serialize, |
| 49 | + Deserialize, |
| 50 | + TraceRawVcs, |
| 51 | + DeterministicHash, |
| 52 | + NonLocalValue, |
| 53 | +)] |
| 54 | +pub enum ContentHashing { |
| 55 | + /// Direct content hashing: Embeds the chunk content hash directly into the referencing chunk. |
| 56 | + /// Benefit: No hash manifest needed. |
| 57 | + /// Downside: Causes cascading hash invalidation. |
| 58 | + Direct { |
| 59 | + /// The length of the content hash in hex chars. Anything lower than 8 is not recommended |
| 60 | + /// due to the high risk of collisions. |
| 61 | + length: u8, |
| 62 | + }, |
| 63 | +} |
| 64 | + |
35 | 65 | pub struct BrowserChunkingContextBuilder {
|
36 | 66 | chunking_context: BrowserChunkingContext,
|
37 | 67 | }
|
@@ -110,6 +140,11 @@ impl BrowserChunkingContextBuilder {
|
110 | 140 | self
|
111 | 141 | }
|
112 | 142 |
|
| 143 | + pub fn use_content_hashing(mut self, content_hashing: ContentHashing) -> Self { |
| 144 | + self.chunking_context.content_hashing = Some(content_hashing); |
| 145 | + self |
| 146 | + } |
| 147 | + |
113 | 148 | pub fn build(self) -> Vc<BrowserChunkingContext> {
|
114 | 149 | BrowserChunkingContext::new(Value::new(self.chunking_context))
|
115 | 150 | }
|
@@ -158,6 +193,8 @@ pub struct BrowserChunkingContext {
|
158 | 193 | runtime_type: RuntimeType,
|
159 | 194 | /// Whether to minify resulting chunks
|
160 | 195 | minify_type: MinifyType,
|
| 196 | + /// Whether content hashing is enabled. |
| 197 | + content_hashing: Option<ContentHashing>, |
161 | 198 | /// Whether to generate source maps
|
162 | 199 | source_maps_type: SourceMapsType,
|
163 | 200 | /// Whether to use manifest chunks for lazy compilation
|
@@ -197,6 +234,7 @@ impl BrowserChunkingContext {
|
197 | 234 | environment,
|
198 | 235 | runtime_type,
|
199 | 236 | minify_type: MinifyType::NoMinify,
|
| 237 | + content_hashing: None, |
200 | 238 | source_maps_type: SourceMapsType::Full,
|
201 | 239 | manifest_chunks: false,
|
202 | 240 | module_id_strategy: ResolvedVc::upcast(DevModuleIdStrategy::new_resolved()),
|
@@ -338,15 +376,35 @@ impl ChunkingContext for BrowserChunkingContext {
|
338 | 376 | #[turbo_tasks::function]
|
339 | 377 | async fn chunk_path(
|
340 | 378 | &self,
|
341 |
| - _asset: Option<Vc<Box<dyn Asset>>>, |
| 379 | + asset: Option<Vc<Box<dyn Asset>>>, |
342 | 380 | ident: Vc<AssetIdent>,
|
343 | 381 | extension: RcStr,
|
344 | 382 | ) -> Result<Vc<FileSystemPath>> {
|
345 | 383 | let root_path = self.chunk_root_path;
|
346 |
| - let name = ident |
347 |
| - .output_name(*self.root_path, extension) |
348 |
| - .owned() |
349 |
| - .await?; |
| 384 | + let name = match self.content_hashing { |
| 385 | + None => { |
| 386 | + ident |
| 387 | + .output_name(*self.root_path, extension) |
| 388 | + .owned() |
| 389 | + .await? |
| 390 | + } |
| 391 | + Some(ContentHashing::Direct { length }) => { |
| 392 | + let Some(asset) = asset else { |
| 393 | + bail!("chunk_path requires an asset when content hashing is enabled"); |
| 394 | + }; |
| 395 | + let content = asset.content().await?; |
| 396 | + if let AssetContent::File(file) = &*content { |
| 397 | + let hash = hash_xxh3_hash64(&file.await?); |
| 398 | + let length = length as usize; |
| 399 | + format!("{hash:0length$x}{extension}").into() |
| 400 | + } else { |
| 401 | + bail!( |
| 402 | + "chunk_path requires an asset with file content when content hashing is \ |
| 403 | + enabled" |
| 404 | + ); |
| 405 | + } |
| 406 | + } |
| 407 | + }; |
350 | 408 | Ok(root_path.join(name))
|
351 | 409 | }
|
352 | 410 |
|
|
0 commit comments