Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions contributing/core/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ we attempt to capture traces of the playwright run to make debugging the failure
A test-trace artifact should be uploaded after the workflow completes which can be downloaded, unzipped,
and then inspected with `pnpm playwright show-trace ./path/to/trace`

To attach the chrome debugger to next the easiest approach is to modify the `createNext` call in your test to pass `--inspect` to next.
To attach the chrome debugger to next the easiest approach is to modify the `nextTestSetup` call in your test to pass `--inspect` to next.

```js
const next = await createNext({
const { next } = nextTestSetup({
...
startArgs: =['--inspect'],
startArgs: ['--inspect'],
})
```

Expand Down
3 changes: 3 additions & 0 deletions crates/next-api/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,7 @@ impl Project {
hash_salt: self.next_config().output_hash_salt().to_resolved().await?,
cross_origin: self.next_config().cross_origin(),
chunk_loading_global: self.next_config().turbopack_chunk_loading_global(),
style_groups_algorithm: self.next_config().css_chunking().owned().await?,
}))
}

Expand Down Expand Up @@ -1629,6 +1630,7 @@ impl Project {
asset_prefix: self.next_config().computed_asset_prefix().owned().await?,
css_url_suffix,
hash_salt: self.next_config().output_hash_salt().to_resolved().await?,
style_groups_algorithm: self.next_config().css_chunking().owned().await?,
};
Ok(if client_assets {
get_server_chunking_context_with_client_assets(options)
Expand Down Expand Up @@ -1669,6 +1671,7 @@ impl Project {
css_url_suffix,
hash_salt: self.next_config().output_hash_salt().to_resolved().await?,
cross_origin: self.next_config().cross_origin(),
style_groups_algorithm: self.next_config().css_chunking().owned().await?,
};
Ok(if client_assets {
get_edge_chunking_context_with_client_assets(options)
Expand Down
47 changes: 33 additions & 14 deletions crates/next-core/src/emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,24 +100,33 @@ pub async fn emit_assets(
path: &FileSystemPath,
assets: AssetVec,
node_root: &FileSystemPath,
) -> Result<ResolvedVc<Box<dyn OutputAsset>>> {
) -> Result<()> {
let mut iter = assets.into_iter();
let first = iter.next().unwrap();
for next in iter {
let ext: RcStr = path.extension().unwrap_or_default().into();
if let Some(detail) = assets_diff(*next, *first, ext, node_root.clone())
.owned()
.await?
{
let ext: RcStr = path.extension().unwrap_or_default().into();
let conflicts = iter
.map(async |next| {
assets_diff(*next, *first, ext.clone(), node_root.clone())
.owned()
.await
})
.try_flat_join()
.await?;
if let Some(detail) = conflicts.into_iter().next() {
#[turbo_tasks::function]
fn emit_conflict_issue(path: FileSystemPath, detail: RcStr) {
EmitConflictIssue {
asset_path: path.clone(),
detail,
}
.resolved_cell()
.emit();
}
emit_conflict_issue(path.clone(), detail)
.as_side_effect()
.await?;
}
Ok(first)
Ok(())
}

// Use join! instead of try_join! to collect all errors deterministically
Expand All @@ -129,14 +138,20 @@ pub async fn emit_assets(
let node_root = node_root.clone();

async move {
let asset = check_duplicates(&path, assets, &node_root).await?;
let asset = *assets.first().unwrap();
let span = tracing::info_span!(
"emit asset",
name = %path.to_string_ref().await?
);
async move { emit(*asset).as_side_effect().await }
.instrument(span)
.await
async move {
emit(*asset).as_side_effect().await?;
// This need to be after `emit()`, so the asset is emitted even if this
// method crashes due to eventual consistency.
check_duplicates(&path, assets, &node_root).await?;
Ok(())
}
.instrument(span)
.await
}
})
.try_join(),
Expand All @@ -148,18 +163,22 @@ pub async fn emit_assets(
let client_output_path = client_output_path.clone();

async move {
let asset = check_duplicates(&path, assets, &node_root).await?;
let span = tracing::info_span!(
"emit asset",
name = %path.to_string_ref().await?
);
async move {
let asset = *assets.first().unwrap();
// Client assets are emitted to the client output path, which is
// prefixed with _next. We need to rebase them to
// remove that prefix.
emit_rebase(*asset, client_relative_path, client_output_path)
.as_side_effect()
.await
.await?;
// This need to be after `emit_rebase()`, so the asset is emitted even if
// this method crashes due to eventual consistency.
check_duplicates(&path, assets, &node_root).await?;
Ok(())
}
.instrument(span)
.await
Expand Down
7 changes: 6 additions & 1 deletion crates/next-core/src/next_client/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ use turbopack_core::{
environment::{BrowserEnvironment, Environment, ExecutionEnvironment},
free_var_references,
issue::IssueSeverity,
module_graph::binding_usage_info::OptionBindingUsageInfo,
module_graph::{
binding_usage_info::OptionBindingUsageInfo, style_groups::StyleGroupsAlgorithm,
},
resolve::{parse::Request, pattern::Pattern},
};
use turbopack_css::chunk::CssChunkType;
Expand Down Expand Up @@ -476,6 +478,7 @@ pub struct ClientChunkingContextOptions {
pub hash_salt: ResolvedVc<RcStr>,
pub cross_origin: Vc<CrossOrigin>,
pub chunk_loading_global: Vc<Option<RcStr>>,
pub style_groups_algorithm: StyleGroupsAlgorithm,
}

#[turbo_tasks::function]
Expand Down Expand Up @@ -505,6 +508,7 @@ pub async fn get_client_chunking_context(
hash_salt,
cross_origin,
chunk_loading_global,
style_groups_algorithm,
} = options;

let next_mode = mode.await?;
Expand Down Expand Up @@ -575,6 +579,7 @@ pub async fn get_client_chunking_context(
Vc::<CssChunkType>::default().to_resolved().await?,
ChunkingConfig {
max_merge_chunk_size: 100_000,
style_groups_algorithm: style_groups_algorithm.clone(),
..Default::default()
},
)
Expand Down
140 changes: 140 additions & 0 deletions crates/next-core/src/next_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use turbopack_core::{
issue::{
IgnoreIssue, IgnoreIssuePattern, Issue, IssueExt, IssueSeverity, IssueStage, StyledString,
},
module_graph::style_groups::StyleGroupsAlgorithm,
resolve::ResolveAliasMap,
};
use turbopack_ecmascript::{OptionTreeShaking, TreeShakingMode};
Expand Down Expand Up @@ -1040,6 +1041,135 @@ pub struct TurbopackIgnoreIssueRule {
pub description: Option<TurbopackIgnoreIssueTextPattern>,
}

/// `experimental.cssChunking` accepts the following shapes (all normalized to a single canonical
/// object form via [`CssChunkingConfig::normalize`]):
///
/// * `true` — equivalent to `{ type: "loose" }` (default loose behaviour).
/// * `false` — disabled chunking.
/// * `"strict"` / `"loose"` / `"graph"` — string shorthands.
/// * `{ type: "strict" }` / `{ type: "loose" }` — object form for the legacy modes.
/// * `{ type: "graph", requestCost?, moduleFactorCost? }` — object form for the graph algorithm.
#[derive(
Clone, Debug, PartialEq, Deserialize, TraceRawVcs, NonLocalValue, OperationValue, Encode, Decode,
)]
#[serde(untagged)]
pub enum CssChunkingConfig {
Bool(bool),
String(CssChunkingMode),
Object(CssChunkingObject),
}

/// String shorthand variants for [`CssChunkingConfig`].
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Deserialize,
TraceRawVcs,
NonLocalValue,
OperationValue,
Encode,
Decode,
)]
#[serde(rename_all = "lowercase")]
pub enum CssChunkingMode {
Strict,
Loose,
Graph,
}

/// Object form of `experimental.cssChunking`.
///
/// `None` is the normalized representation of `false` ("CSS chunking is disabled"). It is not
/// reachable through deserialization — users write `false`, not `{ type: "none" }`.
#[derive(
Clone, Debug, PartialEq, Deserialize, TraceRawVcs, NonLocalValue, OperationValue, Encode, Decode,
)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum CssChunkingObject {
#[serde(skip)]
None,
Strict,
Loose,
Graph(CssChunkingGraphOptions),
}

/// Cost parameters for the graph algorithm. See [`CssChunkingConfig`] for details.
#[derive(
Clone,
Debug,
Default,
PartialEq,
Deserialize,
TraceRawVcs,
NonLocalValue,
OperationValue,
Encode,
Decode,
)]
#[serde(rename_all = "camelCase")]
pub struct CssChunkingGraphOptions {
pub request_cost: Option<f32>,
pub module_factor_cost: Option<f32>,
}

impl CssChunkingConfig {
/// Normalize all input shapes (booleans, strings, object form) to the canonical object form.
/// `false` maps to [`CssChunkingObject::None`]; `true` is equivalent to `'loose'`.
pub fn normalize(&self) -> CssChunkingObject {
match self {
CssChunkingConfig::Bool(false) => CssChunkingObject::None,
CssChunkingConfig::Bool(true) => CssChunkingObject::Loose,
CssChunkingConfig::String(CssChunkingMode::Strict) => CssChunkingObject::Strict,
CssChunkingConfig::String(CssChunkingMode::Loose) => CssChunkingObject::Loose,
CssChunkingConfig::String(CssChunkingMode::Graph) => {
CssChunkingObject::Graph(CssChunkingGraphOptions::default())
}
CssChunkingConfig::Object(obj) => obj.clone(),
}
}
}

/// Default `requestCost` for the graph algorithm (in bytes).
const DEFAULT_REQUEST_COST: f32 = 20_000.0;
/// Default `moduleFactorCost` for the graph algorithm.
const DEFAULT_MODULE_FACTOR_COST: f32 = 1.0;

/// Resolve `experimental.cssChunking` to the [`StyleGroupsAlgorithm`] Turbopack should use.
///
/// `strict` and `false` (`CssChunkingObject::None`) are bundler-incompatible with Turbopack and
/// are rejected at config-validation time on the JS side; if one slips through, we bail rather
/// than silently falling back. `loose` and `true` map to [`StyleGroupsAlgorithm::Default`].
fn resolve_css_chunking_algorithm(
config: Option<&CssChunkingConfig>,
) -> Result<StyleGroupsAlgorithm> {
let Some(config) = config else {
return Ok(StyleGroupsAlgorithm::Default);
};
Ok(match config.normalize() {
CssChunkingObject::None => {
anyhow::bail!(
"`experimental.cssChunking: false` is not supported by Turbopack; this should \
have been rejected at config validation time"
)
}
CssChunkingObject::Strict => {
anyhow::bail!(
"`experimental.cssChunking: \"strict\"` is not supported by Turbopack; this \
should have been rejected at config validation time"
)
}
CssChunkingObject::Loose => StyleGroupsAlgorithm::Default,
CssChunkingObject::Graph(opts) => StyleGroupsAlgorithm::graph(
opts.request_cost.unwrap_or(DEFAULT_REQUEST_COST),
opts.module_factor_cost
.unwrap_or(DEFAULT_MODULE_FACTOR_COST),
),
})
}

#[derive(
Clone,
Debug,
Expand Down Expand Up @@ -1097,6 +1227,9 @@ pub struct ExperimentalConfig {
/// no salt.
output_hash_salt: Option<RcStr>,

/// CSS chunking strategy. See [`CssChunkingConfig`] for the accepted shapes.
css_chunking: Option<CssChunkingConfig>,

// ---
// UNSUPPORTED
// ---
Expand Down Expand Up @@ -1817,6 +1950,13 @@ impl NextConfig {
Vc::cell(self.experimental.inline_css.unwrap_or(false))
}

/// Resolve `experimental.cssChunking` to a [`StyleGroupsAlgorithm`] (with defaults applied
/// for the cost parameters of the graph algorithm).
#[turbo_tasks::function]
pub fn css_chunking(&self) -> Result<Vc<StyleGroupsAlgorithm>> {
Ok(resolve_css_chunking_algorithm(self.experimental.css_chunking.as_ref())?.cell())
}

#[turbo_tasks::function]
pub fn mdx_rs(&self) -> Vc<OptionalMdxTransformOptions> {
let options = &self.experimental.mdx_rs;
Expand Down
9 changes: 8 additions & 1 deletion crates/next-core/src/next_edge/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use turbopack_core::{
environment::{EdgeWorkerEnvironment, Environment, ExecutionEnvironment, NodeJsVersion},
free_var_references,
issue::IssueSeverity,
module_graph::binding_usage_info::OptionBindingUsageInfo,
module_graph::{
binding_usage_info::OptionBindingUsageInfo, style_groups::StyleGroupsAlgorithm,
},
};
use turbopack_css::chunk::CssChunkType;
use turbopack_ecmascript::chunk::EcmascriptChunkType;
Expand Down Expand Up @@ -202,6 +204,7 @@ pub struct EdgeChunkingContextOptions {
pub css_url_suffix: Vc<Option<RcStr>>,
pub hash_salt: ResolvedVc<RcStr>,
pub cross_origin: Vc<CrossOrigin>,
pub style_groups_algorithm: StyleGroupsAlgorithm,
}

/// Like `get_edge_chunking_context` but all assets are emitted as client assets (so `/_next`)
Expand Down Expand Up @@ -229,6 +232,7 @@ pub async fn get_edge_chunking_context_with_client_assets(
css_url_suffix,
hash_salt,
cross_origin,
style_groups_algorithm,
} = options;
let cross_origin_loading = *cross_origin.await?;
let output_root = node_root.join("server/edge")?;
Expand Down Expand Up @@ -280,6 +284,7 @@ pub async fn get_edge_chunking_context_with_client_assets(
Vc::<CssChunkType>::default().to_resolved().await?,
ChunkingConfig {
max_merge_chunk_size: 100_000,
style_groups_algorithm: style_groups_algorithm.clone(),
..Default::default()
},
)
Expand Down Expand Up @@ -314,6 +319,7 @@ pub async fn get_edge_chunking_context(
css_url_suffix,
hash_salt,
cross_origin,
style_groups_algorithm,
} = options;
let cross_origin = *cross_origin.await?;
let css_url_suffix = css_url_suffix.to_resolved().await?;
Expand Down Expand Up @@ -382,6 +388,7 @@ pub async fn get_edge_chunking_context(
Vc::<CssChunkType>::default().to_resolved().await?,
ChunkingConfig {
max_merge_chunk_size: 100_000,
style_groups_algorithm: style_groups_algorithm.clone(),
..Default::default()
},
)
Expand Down
Loading
Loading