Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add node middleware handling for turbopack #76360

Merged
merged 9 commits into from
Feb 24, 2025
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
208 changes: 143 additions & 65 deletions crates/next-api/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use next_core::{
next_edge::entry::wrap_edge_entry,
next_manifests::{EdgeFunctionDefinition, MiddlewareMatcher, MiddlewaresManifestV2, Regions},
next_server::{get_server_runtime_entries, ServerContextType},
util::{parse_config_from_source, MiddlewareMatcherKind},
util::{parse_config_from_source, MiddlewareMatcherKind, NextRuntime},
};
use tracing::Instrument;
use turbo_rcstr::RcStr;
Expand All @@ -16,19 +16,21 @@ use turbo_tasks_fs::{self, File, FileContent, FileSystemPath};
use turbopack_core::{
asset::AssetContent,
chunk::{
availability_info::AvailabilityInfo, ChunkGroupType, ChunkingContextExt, EvaluatableAsset,
availability_info::AvailabilityInfo, ChunkGroupType, ChunkingContext, ChunkingContextExt,
EntryChunkGroupResult, EvaluatableAsset,
},
context::AssetContext,
module::Module,
module_graph::GraphEntries,
output::OutputAssets,
output::{OutputAsset, OutputAssets},
reference_type::{EntryReferenceSubType, ReferenceType},
source::Source,
virtual_output::VirtualOutputAsset,
};
use turbopack_ecmascript::chunk::EcmascriptChunkPlaceable;

use crate::{
nft_json::NftJsonAsset,
paths::{
all_paths_in_root, all_server_paths, get_asset_paths_from_root, get_js_paths_from_root,
get_wasm_paths_from_root, paths_to_bindings, wasm_paths_to_bindings,
Expand All @@ -40,6 +42,7 @@ use crate::{
#[turbo_tasks::value]
pub struct MiddlewareEndpoint {
project: ResolvedVc<Project>,
build_id: RcStr,
asset_context: ResolvedVc<Box<dyn AssetContext>>,
source: ResolvedVc<Box<dyn Source>>,
app_dir: Option<ResolvedVc<FileSystemPath>>,
Expand All @@ -51,13 +54,15 @@ impl MiddlewareEndpoint {
#[turbo_tasks::function]
pub fn new(
project: ResolvedVc<Project>,
build_id: RcStr,
asset_context: ResolvedVc<Box<dyn AssetContext>>,
source: ResolvedVc<Box<dyn Source>>,
app_dir: Option<ResolvedVc<FileSystemPath>>,
ecmascript_client_reference_transition_name: Option<ResolvedVc<RcStr>>,
) -> Vc<Self> {
Self {
project,
build_id,
asset_context,
source,
app_dir,
Expand All @@ -67,7 +72,7 @@ impl MiddlewareEndpoint {
}

#[turbo_tasks::function]
async fn entry_module(&self) -> Vc<Box<dyn Module>> {
async fn entry_module(&self) -> Result<Vc<Box<dyn Module>>> {
let userland_module = self
.asset_context
.process(
Expand All @@ -82,12 +87,17 @@ impl MiddlewareEndpoint {
userland_module,
);

wrap_edge_entry(
let config = parse_config_from_source(userland_module, NextRuntime::Edge).await?;

if matches!(config.runtime, NextRuntime::NodeJs) {
return Ok(module);
}
Ok(wrap_edge_entry(
*self.asset_context,
self.project.project_path(),
module,
"middleware".into(),
)
))
}

#[turbo_tasks::function]
Expand Down Expand Up @@ -134,42 +144,48 @@ impl MiddlewareEndpoint {
}

#[turbo_tasks::function]
async fn output_assets(self: Vc<Self>) -> Result<Vc<OutputAssets>> {
async fn node_chunk(self: Vc<Self>) -> Result<Vc<Box<dyn OutputAsset>>> {
let this = self.await?;

let userland_module = self.userland_module();

let config = parse_config_from_source(userland_module);
let chunking_context = this.project.server_chunking_context(false);

let edge_files = self.edge_files();
let mut output_assets = edge_files.owned().await?;

let node_root = this.project.node_root();
let node_root_value = node_root.await?;

let file_paths_from_root = get_js_paths_from_root(&node_root_value, &output_assets).await?;
let userland_module = self.entry_module().to_resolved().await?;
let module_graph = this
.project
.module_graph(*userland_module, ChunkGroupType::Entry);

let all_output_assets = all_assets_from_entries(edge_files).await?;
let Some(module) = ResolvedVc::try_downcast(userland_module) else {
bail!("Entry module must be evaluatable");
};

let wasm_paths_from_root =
get_wasm_paths_from_root(&node_root_value, &all_output_assets).await?;
let EntryChunkGroupResult { asset: chunk, .. } = *chunking_context
.entry_chunk_group(
this.project.node_root().join("server/middleware.js".into()),
*module,
get_server_runtime_entries(
Value::new(ServerContextType::Middleware {
app_dir: this.app_dir,
ecmascript_client_reference_transition_name: this
.ecmascript_client_reference_transition_name,
}),
this.project.next_mode(),
)
.resolve_entries(*this.asset_context),
module_graph,
OutputAssets::empty(),
Value::new(AvailabilityInfo::Root),
)
.await?;
Ok(*chunk)
}

let all_assets = get_asset_paths_from_root(&node_root_value, &all_output_assets).await?;
#[turbo_tasks::function]
async fn output_assets(self: Vc<Self>) -> Result<Vc<OutputAssets>> {
let this = self.await?;

// Awaited later for parallelism
let config = config.await?;
let userland_module = self.userland_module();

let regions = if let Some(regions) = config.regions.as_ref() {
if regions.len() == 1 {
regions
.first()
.map(|region| Regions::Single(region.clone()))
} else {
Some(Regions::Multiple(regions.clone()))
}
} else {
None
};
let config = parse_config_from_source(userland_module, NextRuntime::Edge).await?;

let next_config = this.project.next_config().await?;
let has_i18n = next_config.i18n.is_some();
Expand Down Expand Up @@ -220,8 +236,9 @@ impl MiddlewareEndpoint {
source.insert_str(0, base_path);
}

// TODO: The implementation of getMiddlewareMatchers outputs a regex here using
// path-to-regexp. Currently there is no equivalent of that so it post-processes
// TODO: The implementation of getMiddlewareMatchers outputs a regex here
// using path-to-regexp. Currently there is no
// equivalent of that so it post-processes
// this value to the relevant regex in manifest-loader.ts
matcher.regexp = Some(RcStr::from(source));

Expand All @@ -236,36 +253,97 @@ impl MiddlewareEndpoint {
}]
};

let edge_function_definition = EdgeFunctionDefinition {
files: file_paths_from_root,
wasm: wasm_paths_to_bindings(wasm_paths_from_root),
assets: paths_to_bindings(all_assets),
name: "middleware".into(),
page: "/".into(),
regions,
matchers,
env: this.project.edge_env().owned().await?,
};
let middleware_manifest_v2 = MiddlewaresManifestV2 {
middleware: [("/".into(), edge_function_definition)]
.into_iter()
.collect(),
..Default::default()
};
let middleware_manifest_v2 = VirtualOutputAsset::new(
node_root.join("server/middleware/middleware-manifest.json".into()),
AssetContent::file(
FileContent::Content(File::from(serde_json::to_string_pretty(
&middleware_manifest_v2,
)?))
.cell(),
),
)
.to_resolved()
.await?;
output_assets.push(ResolvedVc::upcast(middleware_manifest_v2));
if matches!(config.runtime, NextRuntime::NodeJs) {
let chunk = self.node_chunk().to_resolved().await?;
let mut output_assets = vec![chunk];
if this.project.next_mode().await?.is_production() {
output_assets.push(ResolvedVc::upcast(
NftJsonAsset::new(*this.project, *chunk, vec![])
.to_resolved()
.await?,
));
}
let middleware_manifest_v2 = MiddlewaresManifestV2 {
middleware: [].into_iter().collect(),
..Default::default()
};
Comment on lines +266 to +269
Copy link
Contributor

Choose a reason for hiding this comment

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

This manifest is completely empty. Is that intentional?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, node middleware uses different manifest which is generated separate

Copy link
Contributor

Choose a reason for hiding this comment

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

But does the JS-side of Next.js really need this empty file then?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yea it's always expected to be present also makes it more clear we didn't just fail to generate a manifest.

let middleware_manifest_v2 = VirtualOutputAsset::new(
this.project
.node_root()
.join("server/middleware/middleware-manifest.json".into()),
AssetContent::file(
FileContent::Content(File::from(serde_json::to_string_pretty(
&middleware_manifest_v2,
)?))
.cell(),
),
)
.to_resolved()
.await?;
output_assets.push(ResolvedVc::upcast(middleware_manifest_v2));

Ok(Vc::cell(output_assets))
} else {
let edge_files = self.edge_files();
let mut output_assets = edge_files.owned().await?;

let node_root = this.project.node_root();
let node_root_value = node_root.await?;

let file_paths_from_root =
get_js_paths_from_root(&node_root_value, &output_assets).await?;

let all_output_assets = all_assets_from_entries(edge_files).await?;

let wasm_paths_from_root =
get_wasm_paths_from_root(&node_root_value, &all_output_assets).await?;

Ok(Vc::cell(output_assets))
let all_assets =
get_asset_paths_from_root(&node_root_value, &all_output_assets).await?;

let regions = if let Some(regions) = config.regions.as_ref() {
if regions.len() == 1 {
regions
.first()
.map(|region| Regions::Single(region.clone()))
} else {
Some(Regions::Multiple(regions.clone()))
}
} else {
None
};

let edge_function_definition = EdgeFunctionDefinition {
files: file_paths_from_root,
wasm: wasm_paths_to_bindings(wasm_paths_from_root),
assets: paths_to_bindings(all_assets),
name: "middleware".into(),
page: "/".into(),
regions,
matchers: matchers.clone(),
env: this.project.edge_env().owned().await?,
};
let middleware_manifest_v2 = MiddlewaresManifestV2 {
middleware: [("/".into(), edge_function_definition)]
.into_iter()
.collect(),
..Default::default()
};
let middleware_manifest_v2 = VirtualOutputAsset::new(
node_root.join("server/middleware/middleware-manifest.json".into()),
AssetContent::file(
FileContent::Content(File::from(serde_json::to_string_pretty(
&middleware_manifest_v2,
)?))
.cell(),
),
)
.to_resolved()
.await?;
output_assets.push(ResolvedVc::upcast(middleware_manifest_v2));

Ok(Vc::cell(output_assets))
}
}

#[turbo_tasks::function]
Expand Down
2 changes: 1 addition & 1 deletion crates/next-api/src/pages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,7 @@ impl PageEndpoint {
.process(self.source(), reference_type.clone())
.module();

let config = parse_config_from_source(ssr_module).await?;
let config = parse_config_from_source(ssr_module, NextRuntime::default()).await?;
let is_edge = matches!(config.runtime, NextRuntime::Edge);

let ssr_module = if is_edge {
Expand Down
Loading
Loading