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
129 changes: 91 additions & 38 deletions crates/next-api/src/app.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::{Context, Result};
use anyhow::{Context, Result, bail};
use bincode::{Decode, Encode};
use next_core::{
app_structure::{
Expand Down Expand Up @@ -39,8 +39,8 @@ use next_core::{
use tracing::{Instrument, field::Empty};
use turbo_rcstr::{RcStr, rcstr};
use turbo_tasks::{
Completion, FxIndexMap, NonLocalValue, ResolvedVc, TryJoinIterExt, ValueToString, Vc,
fxindexset, trace::TraceRawVcs,
Completion, FxIndexMap, NonLocalValue, OperationVc, ResolvedVc, TryJoinIterExt, ValueToString,
Vc, fxindexset, trace::TraceRawVcs,
};
use turbo_tasks_fs::{File, FileContent, FileSystemPath};
use turbopack::{
Expand Down Expand Up @@ -78,6 +78,8 @@ use crate::{
module_graph::{ClientReferencesGraphs, NextDynamicGraphs, ServerActionsGraphs},
nft::{EndpointTraceResult, trace_endpoint},
nft_json::NftJsonAsset,
operation::OptionEndpoint,
output_mode::{OptionSsrMarkTarget, SsrMarkTarget},
paths::{
all_asset_paths, all_paths_in_root, get_asset_paths_from_root, get_js_paths_from_root,
get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings,
Expand Down Expand Up @@ -1106,9 +1108,49 @@ pub fn app_entry_point_to_route(
.cell()
}

/// Resolves the [`crate::output_mode::OutputModeState`] and page key for an
/// app page HTML endpoint, so that [`crate::output_mode::mark_as_ssr`] can
/// insert the page.
#[turbo_tasks::function(operation, root)]
pub(crate) async fn mark_as_ssr_operation(
endpoint_op: OperationVc<OptionEndpoint>,
) -> Result<Vc<OptionSsrMarkTarget>> {
// Skip marking if the endpoint fails to resolve.
let Some(endpoint) = endpoint_op.connect().await.ok().and_then(|e| *e) else {
return Ok(Vc::cell(None));
};
let Some(app_endpoint) = ResolvedVc::try_downcast_type::<AppEndpoint>(endpoint) else {
bail!("mark_as_ssr is only called for app pages");
};
let app_endpoint = app_endpoint.await?;
if !matches!(
app_endpoint.ty,
AppEndpointType::Page {
ty: AppPageEndpointType::Html,
..
}
) {
bail!("mark_as_ssr is only called for app page HTML endpoints");
}
let Some(state) = *app_endpoint
.app_project
.project()
.output_mode_state()
.await?
else {
bail!("mark_as_ssr is never called outside of a dev session");
};
Ok(Vc::cell(Some(SsrMarkTarget {
state,
page: app_endpoint.page.to_string().into(),
})))
}

#[derive(Copy, Clone, PartialEq, Eq, Debug, TraceRawVcs, NonLocalValue, Encode, Decode)]
enum AppPageEndpointType {
Html,
/// HMR-only: detects Server Component changes but emits no manifests, so it
/// cannot serve a request.
RscHmr,
}

Expand Down Expand Up @@ -1232,26 +1274,38 @@ impl AppEndpoint {
/// All manifests: `Minimal` plus next-font, next-dynamic, ...
Full,
}
let (process_client_assets, process_ssr, emit_manifests, emit_rsc_manifests) =
match &this.ty {
AppEndpointType::Page { ty, .. } => (
true,
matches!(ty, AppPageEndpointType::Html),
if matches!(ty, AppPageEndpointType::Html) {
EmitManifests::Full
} else {
EmitManifests::None
},
matches!(ty, AppPageEndpointType::Html),
),
AppEndpointType::Route { .. } => (false, false, EmitManifests::Minimal, true),
AppEndpointType::Metadata { metadata } => (
false,
false,
EmitManifests::Minimal,
matches!(metadata, MetadataItem::Dynamic { .. }),
),
};
let (process_client_assets, process_ssr, emit_manifests, emit_rsc_manifests) = match &this
.ty
{
AppEndpointType::Page { ty, .. } => (
true,
match ty {
AppPageEndpointType::Html => {
match &*project.output_mode_state().await? {
// In development, skip building the Client Component SSR
// chunks until the page has been rendered as a document.
// A page only ever reached through RSC-only soft
// navigations never needs to compile its SSR output.
Some(state) => *state.is_ssr_page(this.page.to_string().into()).await?,
None => true,
}
}
AppPageEndpointType::RscHmr => false,
},
match ty {
AppPageEndpointType::Html => EmitManifests::Full,
AppPageEndpointType::RscHmr => EmitManifests::None,
},
matches!(ty, AppPageEndpointType::Html),
),
AppEndpointType::Route { .. } => (false, false, EmitManifests::Minimal, true),
AppEndpointType::Metadata { metadata } => (
false,
false,
EmitManifests::Minimal,
matches!(metadata, MetadataItem::Dynamic { .. }),
),
};

let node_root = project.node_root().owned().await?;
let client_relative_path = project.client_relative_path().owned().await?;
Expand Down Expand Up @@ -1280,21 +1334,17 @@ impl AppEndpoint {

let client_chunking_context = project.client_chunking_context().to_resolved().await?;

let ssr_chunking_context = if process_ssr {
Some(
match runtime {
NextRuntime::NodeJs => Vc::upcast(project.server_chunking_context(true)),
NextRuntime::Edge => this
.app_project
.project()
.edge_chunking_context(process_client_assets),
}
.to_resolved()
.await?,
)
} else {
None
};
let server_chunking_context = match runtime {
NextRuntime::NodeJs => Vc::upcast(project.server_chunking_context(true)),
NextRuntime::Edge => this
.app_project
.project()
.edge_chunking_context(process_client_assets),
}
.to_resolved()
.await?;

Comment on lines +1337 to +1346

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we always resolve the chunking context if we might not need it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're still using this for RSC

let ssr_chunking_context = process_ssr.then_some(server_chunking_context);

let per_page_module_graph = *project.per_page_module_graph().await?;

Expand Down Expand Up @@ -1548,6 +1598,9 @@ impl AppEndpoint {
client_references_chunks,
client_chunking_context,
ssr_chunking_context,
// Only pages need `rscModuleMapping`; route handlers and
// metadata routes keep emitting no module mappings.
rsc_chunking_context: is_app_page.then_some(server_chunking_context),
async_module_info: module_graphs.full.async_module_info().to_resolved().await?,
next_config: project.next_config().to_resolved().await?,
runtime,
Expand Down
1 change: 1 addition & 0 deletions crates/next-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub mod next_server_nft;
mod nft;
mod nft_json;
pub mod operation;
pub mod output_mode;
mod pages;
pub mod paths;
pub mod project;
Expand Down
67 changes: 67 additions & 0 deletions crates/next-api/src/output_mode.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use anyhow::Result;
use bincode::{Decode, Encode};
use rustc_hash::FxHashSet;
use turbo_rcstr::RcStr;
use turbo_tasks::{
NonLocalValue, OperationVc, ResolvedVc, State, Vc, debug::ValueDebugFormat, trace::TraceRawVcs,
};

use crate::{app::mark_as_ssr_operation, operation::OptionEndpoint};

#[turbo_tasks::value]
pub struct OutputModeState {
/// App pages that must emit full HTML output, including Client Component
/// SSR chunks. Insert-only: once a page has been rendered as a document it
/// never downgrades to the SSR-free output.
ssr_pages: State<FxHashSet<RcStr>>,
}

impl OutputModeState {
/// This must not be a `#[turbo_tasks::function]` because it should be a
/// singleton for each project.
pub fn new() -> ResolvedVc<Self> {
OutputModeState {
ssr_pages: State::new(FxHashSet::default()),
}
.resolved_cell()
}

fn mark_ssr(&self, page: RcStr) {
self.ssr_pages
.update_conditionally(|pages| pages.insert(page));
}
}

#[turbo_tasks::value_impl]
impl OutputModeState {
#[turbo_tasks::function]
pub fn is_ssr_page(&self, page: RcStr) -> Vc<bool> {
Vc::cell(self.ssr_pages.get().contains(&page))
}
}

#[turbo_tasks::value(transparent)]
pub struct OptionOutputModeState(Option<ResolvedVc<OutputModeState>>);

#[derive(
TraceRawVcs, PartialEq, Eq, ValueDebugFormat, Clone, Debug, NonLocalValue, Encode, Decode,
)]
pub struct SsrMarkTarget {
pub state: ResolvedVc<OutputModeState>,
pub page: RcStr,
}

#[turbo_tasks::value(transparent)]
pub struct OptionSsrMarkTarget(Option<SsrMarkTarget>);

/// Marks the app page served by `endpoint_op` as requiring full HTML output
/// from now on. No-op for endpoints other than app page HTML endpoints.
pub async fn mark_as_ssr(endpoint_op: OperationVc<OptionEndpoint>) -> Result<()> {
let target = mark_as_ssr_operation(endpoint_op)
.read_strongly_consistent()
.await?;
if let Some(target) = &*target {
target.state.await?.mark_ssr(target.page.clone());
}
Ok(())
}
20 changes: 20 additions & 0 deletions crates/next-api/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ use crate::{
entrypoints::Entrypoints,
instrumentation::InstrumentationEndpoint,
middleware::MiddlewareEndpoint,
output_mode::{OptionOutputModeState, OutputModeState},
pages::PagesProject,
route::{
Endpoint, EndpointGroup, EndpointGroupEntry, EndpointGroupKey, EndpointGroups, Endpoints,
Expand Down Expand Up @@ -466,6 +467,7 @@ pub struct ProjectContainer {
name: RcStr,
options_state: State<Option<ProjectOptions>>,
versioned_content_map: Option<ResolvedVc<VersionedContentMap>>,
output_mode_state: Option<ResolvedVc<OutputModeState>>,
}

#[turbo_tasks::value_impl]
Expand All @@ -481,6 +483,13 @@ impl ProjectContainer {
} else {
None
},
// only dev serves pages on demand, so only dev can defer the
// Client Component SSR output of a page
output_mode_state: if dev {
Some(OutputModeState::new())
} else {
None
},
options_state: State::new(None),
}
.cell())
Expand Down Expand Up @@ -866,6 +875,7 @@ impl ProjectContainer {
NextMode::Build.resolved_cell()
},
versioned_content_map: self.versioned_content_map,
output_mode_state: self.output_mode_state,
build_id,
encryption_key,
preview_props,
Expand Down Expand Up @@ -951,6 +961,11 @@ pub struct Project {

versioned_content_map: Option<ResolvedVc<VersionedContentMap>>,

/// Tracks which app pages must emit full HTML output in development. Like
/// [`Project::versioned_content_map`], this is the same cell across
/// `Project` re-creations, so the state survives options changes.
output_mode_state: Option<ResolvedVc<OutputModeState>>,

build_id: RcStr,

encryption_key: RcStr,
Expand Down Expand Up @@ -1057,6 +1072,11 @@ impl Project {
PagesProject::new(self)
}

#[turbo_tasks::function]
pub fn output_mode_state(&self) -> Vc<OptionOutputModeState> {
Vc::cell(self.output_mode_state)
}

#[turbo_tasks::function]
pub fn project_fs(&self) -> Result<Vc<DiskFileSystem>> {
let denied_path = match join_path(&self.project_path, &self.dist_dir_root) {
Expand Down
Loading
Loading