|
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},
|
@@ -42,6 +47,31 @@ pub enum CurrentChunkMethod {
|
42 | 47 | pub const CURRENT_CHUNK_METHOD_DOCUMENT_CURRENT_SCRIPT_EXPR: &str =
|
43 | 48 | "typeof document === \"object\" ? document.currentScript : undefined";
|
44 | 49 |
|
| 50 | +#[derive( |
| 51 | + Debug, |
| 52 | + TaskInput, |
| 53 | + Clone, |
| 54 | + Copy, |
| 55 | + PartialEq, |
| 56 | + Eq, |
| 57 | + Hash, |
| 58 | + Serialize, |
| 59 | + Deserialize, |
| 60 | + TraceRawVcs, |
| 61 | + DeterministicHash, |
| 62 | + NonLocalValue, |
| 63 | +)] |
| 64 | +pub enum ContentHashing { |
| 65 | + /// Direct content hashing: Embeds the chunk content hash directly into the referencing chunk. |
| 66 | + /// Benefit: No hash manifest needed. |
| 67 | + /// Downside: Causes cascading hash invalidation. |
| 68 | + Direct { |
| 69 | + /// The length of the content hash in hex chars. Anything lower than 8 is not recommended |
| 70 | + /// due to the high risk of collisions. |
| 71 | + length: u8, |
| 72 | + }, |
| 73 | +} |
| 74 | + |
45 | 75 | pub struct BrowserChunkingContextBuilder {
|
46 | 76 | chunking_context: BrowserChunkingContext,
|
47 | 77 | }
|
@@ -125,6 +155,11 @@ impl BrowserChunkingContextBuilder {
|
125 | 155 | self
|
126 | 156 | }
|
127 | 157 |
|
| 158 | + pub fn use_content_hashing(mut self, content_hashing: ContentHashing) -> Self { |
| 159 | + self.chunking_context.content_hashing = Some(content_hashing); |
| 160 | + self |
| 161 | + } |
| 162 | + |
128 | 163 | pub fn build(self) -> Vc<BrowserChunkingContext> {
|
129 | 164 | BrowserChunkingContext::new(Value::new(self.chunking_context))
|
130 | 165 | }
|
@@ -173,6 +208,8 @@ pub struct BrowserChunkingContext {
|
173 | 208 | runtime_type: RuntimeType,
|
174 | 209 | /// Whether to minify resulting chunks
|
175 | 210 | minify_type: MinifyType,
|
| 211 | + /// Whether content hashing is enabled. |
| 212 | + content_hashing: Option<ContentHashing>, |
176 | 213 | /// Whether to generate source maps
|
177 | 214 | source_maps_type: SourceMapsType,
|
178 | 215 | /// Method to use when figuring out the current chunk src
|
@@ -214,6 +251,7 @@ impl BrowserChunkingContext {
|
214 | 251 | environment,
|
215 | 252 | runtime_type,
|
216 | 253 | minify_type: MinifyType::NoMinify,
|
| 254 | + content_hashing: None, |
217 | 255 | source_maps_type: SourceMapsType::Full,
|
218 | 256 | current_chunk_method: CurrentChunkMethod::StringLiteral,
|
219 | 257 | manifest_chunks: false,
|
@@ -361,15 +399,35 @@ impl ChunkingContext for BrowserChunkingContext {
|
361 | 399 | #[turbo_tasks::function]
|
362 | 400 | async fn chunk_path(
|
363 | 401 | &self,
|
364 |
| - _asset: Option<Vc<Box<dyn Asset>>>, |
| 402 | + asset: Option<Vc<Box<dyn Asset>>>, |
365 | 403 | ident: Vc<AssetIdent>,
|
366 | 404 | extension: RcStr,
|
367 | 405 | ) -> Result<Vc<FileSystemPath>> {
|
368 | 406 | let root_path = self.chunk_root_path;
|
369 |
| - let name = ident |
370 |
| - .output_name(*self.root_path, extension) |
371 |
| - .owned() |
372 |
| - .await?; |
| 407 | + let name = match self.content_hashing { |
| 408 | + None => { |
| 409 | + ident |
| 410 | + .output_name(*self.root_path, extension) |
| 411 | + .owned() |
| 412 | + .await? |
| 413 | + } |
| 414 | + Some(ContentHashing::Direct { length }) => { |
| 415 | + let Some(asset) = asset else { |
| 416 | + bail!("chunk_path requires an asset when content hashing is enabled"); |
| 417 | + }; |
| 418 | + let content = asset.content().await?; |
| 419 | + if let AssetContent::File(file) = &*content { |
| 420 | + let hash = hash_xxh3_hash64(&file.await?); |
| 421 | + let length = length as usize; |
| 422 | + format!("{hash:0length$x}{extension}").into() |
| 423 | + } else { |
| 424 | + bail!( |
| 425 | + "chunk_path requires an asset with file content when content hashing is \ |
| 426 | + enabled" |
| 427 | + ); |
| 428 | + } |
| 429 | + } |
| 430 | + }; |
373 | 431 | Ok(root_path.join(name))
|
374 | 432 | }
|
375 | 433 |
|
|
0 commit comments