From 3ba5109c95cfb3abba4def24f2038314e329ae5f Mon Sep 17 00:00:00 2001 From: Alex Kirszenberg Date: Fri, 28 Jul 2023 15:41:26 +0200 Subject: [PATCH 01/15] Temporary middleware manifest work --- packages/next-swc/crates/next-api/src/app.rs | 30 ++ .../crates/next-core/src/next_edge/mod.rs | 1 + .../next-core/src/next_edge/route_regex.rs | 260 ++++++++++++++++++ .../next-core/src/next_manifests/mod.rs | 67 ++++- 4 files changed, 356 insertions(+), 2 deletions(-) create mode 100644 packages/next-swc/crates/next-core/src/next_edge/route_regex.rs diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index b21ad69ec737a..c663230472758 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -621,6 +621,36 @@ impl AppEndpoint { ); output_assets.push(entry_manifest); + // TODO(alexkirsz) Expose config and type: edge/normal on AppEntry. + // TODO(alexkirsz) This should be shared with next build. + if let Some(edge_config) = &app_entry.edge_config { + let named_regex = get_named_middleware_regex(&app_entry.pathname); + let matchers = MiddlewareMatcher { + regexp: named_regex, + original_source: app_entry.pathname.clone(), + ..Default::default() + }; + let edge_function_definition = EdgeFunctionDefinition { + files: todo!(), // get_entry_files(...), + name: "".to_string(), + page: app_entry.original_name.clone(), + regions: edge_config + .await? + .preferred_region + .map(|region| Regions::Single(region)), + matchers: vec![matchers], + ..Default::default() + }; + let middleware_manifest_v2 = MiddlewaresManifestV2 { + sorted_middleware: vec![original_name.clone()], + middleware: Default::default(), + functions: [(original_name, edge_function_definition)] + .into_iter() + .collect(), + }; + // TODO(alexkirsz) Add the manifest. + } + fn create_app_paths_manifest( node_root: Vc, original_name: &str, diff --git a/packages/next-swc/crates/next-core/src/next_edge/mod.rs b/packages/next-swc/crates/next-core/src/next_edge/mod.rs index 1ee55be5bcbc3..8f8bcfae99304 100644 --- a/packages/next-swc/crates/next-core/src/next_edge/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_edge/mod.rs @@ -1,3 +1,4 @@ pub mod context; pub mod page_transition; +pub(crate) mod route_regex; pub mod route_transition; diff --git a/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs b/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs new file mode 100644 index 0000000000000..405ccd902f6e1 --- /dev/null +++ b/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs @@ -0,0 +1,260 @@ +//! The following code was mostly generated using GTP-4 from +//! next.js/packages/next/src/shared/lib/router/utils/route-regex.ts +//! +//! It contains errors and is not meant to be used as-is. +//! +//! The following should be changed: +//! * NamedMiddlewareRegex should just contain a string, not an actual Regex. +//! * There's plenty of places where more Rust-idiomatic string processing could +//! be used. +//! * Compilation errors. + +use std::collections::HashMap; + +use lazy_static::lazy_static; +use regex::Regex; + +const INTERCEPTION_ROUTE_MARKERS: [&str; 0] = []; // Filler value +const NEXT_QUERY_PARAM_PREFIX: &str = "nxtP"; +const NEXT_INTERCEPTION_MARKER_PREFIX: &str = "nxtI"; + +#[derive(Debug, Clone)] +pub struct Group { + pub pos: usize, + pub repeat: bool, + pub optional: bool, +} + +#[derive(Debug)] +pub struct RouteRegex { + pub groups: HashMap, + pub re: Regex, +} + +#[derive(Debug)] +pub struct NamedRouteRegex { + pub regex: RouteRegex, + pub named_regex: Regex, + pub route_keys: HashMap, +} + +#[derive(Debug)] +pub struct NamedMiddlewareRegex { + pub named_regex: Regex, +} + +/// Parses a given parameter from a route to a data structure that can be used +/// to generate the parametrized route. Examples: +/// - `[...slug]` -> `{ key: 'slug', repeat: true, optional: true }` +/// - `...slug` -> `{ key: 'slug', repeat: true, optional: false }` +/// - `[foo]` -> `{ key: 'foo', repeat: false, optional: true }` +/// - `bar` -> `{ key: 'bar', repeat: false, optional: false }` +fn parse_parameter(param: &str) -> (String, bool, bool) { + let mut param = param.to_string(); + let optional = param.starts_with('[') && param.ends_with(']'); + if optional { + param = param[1..param.len() - 1].to_string(); + } + let repeat = param.starts_with("..."); + if repeat { + param = param[3..].to_string(); + } + (param, repeat, optional) +} + +fn escape_string_regexp(segment: &str) -> String { + regex::escape(segment) +} + +fn remove_trailing_slash(route: &str) -> String { + route.trim_end_matches('/').to_string() +} + +lazy_static! { + static ref PARAM_MATCH_REGEX: Regex = Regex::new(r"\[((?:\[.*\])|.+)\]").unwrap(); +} + +fn get_parametrized_route(route: &str) -> (String, HashMap) { + let segments: Vec<&str> = remove_trailing_slash(route)[1..].split('/').collect(); + let mut groups: HashMap = HashMap::new(); + let mut group_index = 1; + let parameterized_route = segments + .iter() + .map(|segment| { + let marker_match = INTERCEPTION_ROUTE_MARKERS + .iter() + .find(|m| segment.starts_with(m)); + let param_matches = PARAM_MATCH_REGEX.captures(segment); + if let Some(marker) = marker_match { + if let Some(matches) = param_matches { + let (key, optional, repeat) = parse_parameter(&matches[1]); + groups.insert( + key.clone(), + Group { + pos: group_index, + repeat, + optional, + }, + ); + group_index += 1; + return format!("/{}([^/]+?)", escape_string_regexp(marker)); + } + } else if let Some(matches) = param_matches { + let (key, optional, repeat) = parse_parameter(&matches[1]); + groups.insert( + key.clone(), + Group { + pos: group_index, + repeat, + optional, + }, + ); + group_index += 1; + return if repeat { + if optional { + "(?:/(.+?))?" + } else { + "/(.+?)" + } + } else { + "/([^/]+?)" + } + .to_string(); + } + format!("/{}", escape_string_regexp(segment)) + }) + .collect::>() + .join(""); + (parameterized_route, groups) +} + +/// From a normalized route this function generates a regular expression and +/// a corresponding groups object intended to be used to store matching groups +/// from the regular expression. +pub fn get_route_regex(normalized_route: &str) -> RouteRegex { + let (parameterized_route, groups) = get_parametrized_route(normalized_route); + RouteRegex { + re: Regex::new(&format!("^{}(?:/)?$", parameterized_route)).unwrap(), + groups, + } +} + +/// Builds a function to generate a minimal routeKey using only a-z and minimal +/// number of characters. +fn build_get_safe_route_key() -> Box String> { + let mut route_key_char_code = 97; + let mut route_key_char_length = 1; + Box::new(move || { + let mut route_key = String::new(); + for _ in 0..route_key_char_length { + route_key.push(route_key_char_code as u8 as char); + route_key_char_code += 1; + if route_key_char_code > 122 { + route_key_char_length += 1; + route_key_char_code = 97; + } + } + route_key + }) +} + +fn get_safe_key_from_segment( + segment: &str, + route_keys: &mut HashMap, + key_prefix: Option, +) -> String { + let mut get_safe_route_key = build_get_safe_route_key(); + let (key, optional, repeat) = parse_parameter(segment); + + // replace any non-word characters since they can break + // the named regex + let mut cleaned_key = key.replace(|c: char| !c.is_alphanumeric(), ""); + if let Some(prefix) = key_prefix { + cleaned_key = format!("{}{}", prefix, cleaned_key); + } + let mut invalid_key = false; + + // check if the key is still invalid and fallback to using a known + // safe key + if cleaned_key.is_empty() || cleaned_key.len() > 30 { + invalid_key = true; + } + if cleaned_key.chars().next().unwrap().is_numeric() { + invalid_key = true; + } + if invalid_key { + cleaned_key = get_safe_route_key(); + } + if let Some(prefix) = key_prefix { + route_keys.insert(cleaned_key.clone(), format!("{}{}", prefix, key)); + } else { + route_keys.insert(cleaned_key.clone(), key); + } + if repeat { + if optional { + format!(r"(?:/(?P<{}>.+?))?", cleaned_key) + } else { + format!(r"/(?P<{}>.+?)", cleaned_key) + } + } else { + format!(r"/(?P<{}>[^/]+?)", cleaned_key) + } +} + +fn get_named_parametrized_route( + route: &str, + prefix_route_keys: bool, +) -> (String, HashMap) { + let segments: Vec<&str> = remove_trailing_slash(route)[1..].split('/').collect(); + let mut route_keys: HashMap = HashMap::new(); + let parameterized_route = segments + .iter() + .map(|segment| { + let marker_match = INTERCEPTION_ROUTE_MARKERS + .iter() + .find(|m| segment.starts_with(m)); + let param_matches = Regex::new(r"\[((?:\[.*\])|.+)\]") + .unwrap() + .captures(segment); + if let Some(marker) = marker_match { + if let Some(matches) = param_matches { + return get_safe_key_from_segment( + &matches[1], + &mut route_keys, + Some(escape_string_regexp(marker)), + ); + } + } else if let Some(matches) = param_matches { + return get_safe_key_from_segment(&matches[1], &mut route_keys, None); + } + format!("/{}", escape_string_regexp(segment)) + }) + .collect::>() + .join(""); + (parameterized_route, route_keys) +} + +/// This function extends `getRouteRegex` generating also a named regexp where +/// each group is named along with a routeKeys object that indexes the assigned +/// named group with its corresponding key. When the routeKeys need to be +/// prefixed to uniquely identify internally the "prefixRouteKey" arg should +/// be "true" currently this is only the case when creating the routes-manifest +/// during the build +pub fn get_named_route_regex(normalized_route: &str) -> NamedRouteRegex { + let (parameterized_route, route_keys) = get_named_parametrized_route(normalized_route, false); + let regex = get_route_regex(normalized_route); + NamedRouteRegex { + regex, + named_regex: Regex::new(&format!("^{}(?:/)?$", parameterized_route)).unwrap(), + route_keys, + } +} + +/// Generates a named regexp. +/// This is intended to be using for build time only. +pub fn get_named_middleware_regex(normalized_route: &str) -> NamedMiddlewareRegex { + let (parameterized_route, _route_keys) = get_named_parametrized_route(normalized_route, true); + NamedMiddlewareRegex { + named_regex: Regex::new(&format!("^{}(?:/)?$", parameterized_route)).unwrap(), + } +} diff --git a/packages/next-swc/crates/next-core/src/next_manifests/mod.rs b/packages/next-swc/crates/next-core/src/next_manifests/mod.rs index 74559235f0e24..82bc7336bdc68 100644 --- a/packages/next-swc/crates/next-core/src/next_manifests/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_manifests/mod.rs @@ -41,11 +41,74 @@ impl Default for MiddlewaresManifest { } } +#[derive(Serialize, Debug)] +#[serde(tag = "type", rename_all = "kebab-case")] +pub enum RouteHas { + Header { + key: String, + #[serde(skip_serializing_if = "Option::is_none")] + value: Option, + }, + Cookie { + key: String, + #[serde(skip_serializing_if = "Option::is_none")] + value: Option, + }, + Query { + key: String, + #[serde(skip_serializing_if = "Option::is_none")] + value: Option, + }, + Host { + value: String, + }, +} + +#[derive(Serialize, Default, Debug)] +#[serde(rename_all = "camelCase")] +pub struct MiddlewareMatcher { + regexp: String, + #[serde(skip_serializing_if = "bool_is_true")] + locale: bool, + #[serde(skip_serializing_if = "Option::is_none")] + has: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + missing: Option>, + original_source: String, +} + +fn bool_is_true(b: &bool) -> bool { + *b +} + +#[derive(Serialize, Default, Debug)] +pub struct EdgeFunctionDefinition { + files: Vec, + name: String, + page: String, + matchers: Vec, + // TODO: AssetBinding[] + #[serde(skip_serializing_if = "Option::is_none")] + wasm: Option>, + // TODO: AssetBinding[] + #[serde(skip_serializing_if = "Option::is_none")] + assets: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + regions: Option, +} + +#[derive(Serialize, Debug)] +#[serde(untagged)] +pub enum Regions { + Multiple(Vec), + Single(String), +} + #[derive(Serialize, Default, Debug)] pub struct MiddlewaresManifestV2 { - pub sorted_middleware: Vec<()>, + pub sorted_middleware: Vec, pub middleware: HashMap, - pub functions: HashMap, + pub functions: HashMap, } #[derive(Serialize, Default, Debug)] From 093cf7c9e5d06e41fd0800b805f95f457449205e Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Mon, 31 Jul 2023 13:39:47 +0000 Subject: [PATCH 02/15] add middlewaremanifest and fix issues --- packages/next-swc/crates/next-api/src/app.rs | 169 ++++++++++++------ packages/next-swc/crates/next-core/src/lib.rs | 2 +- .../next-core/src/next_app/app_route_entry.rs | 63 +++++-- .../crates/next-core/src/next_edge/context.rs | 4 +- .../crates/next-core/src/next_edge/mod.rs | 2 +- .../next-core/src/next_edge/route_regex.rs | 154 ++++++++-------- .../next-core/src/next_manifests/mod.rs | 24 +-- .../src/server/lib/router-utils/setup-dev.ts | 55 ++++-- .../shared/lib/router/utils/route-regex.ts | 7 +- 9 files changed, 300 insertions(+), 180 deletions(-) diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index c663230472758..3e78345b24122 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use next_core::{ all_server_paths, app_structure::{ @@ -18,8 +18,10 @@ use next_core::{ ClientReferenceGraph, ClientReferenceType, NextEcmascriptClientReferenceTransition, }, next_dynamic::{NextDynamicEntries, NextDynamicTransition}, + next_edge::route_regex::get_named_middleware_regex, next_manifests::{ - AppBuildManifest, AppPathsManifest, BuildManifest, ClientReferenceManifest, PagesManifest, + AppBuildManifest, AppPathsManifest, BuildManifest, ClientReferenceManifest, + EdgeFunctionDefinition, MiddlewareMatcher, MiddlewaresManifestV2, PagesManifest, Regions, }, next_server::{ get_server_module_options_context, get_server_resolve_options_context, @@ -621,36 +623,6 @@ impl AppEndpoint { ); output_assets.push(entry_manifest); - // TODO(alexkirsz) Expose config and type: edge/normal on AppEntry. - // TODO(alexkirsz) This should be shared with next build. - if let Some(edge_config) = &app_entry.edge_config { - let named_regex = get_named_middleware_regex(&app_entry.pathname); - let matchers = MiddlewareMatcher { - regexp: named_regex, - original_source: app_entry.pathname.clone(), - ..Default::default() - }; - let edge_function_definition = EdgeFunctionDefinition { - files: todo!(), // get_entry_files(...), - name: "".to_string(), - page: app_entry.original_name.clone(), - regions: edge_config - .await? - .preferred_region - .map(|region| Regions::Single(region)), - matchers: vec![matchers], - ..Default::default() - }; - let middleware_manifest_v2 = MiddlewaresManifestV2 { - sorted_middleware: vec![original_name.clone()], - middleware: Default::default(), - functions: [(original_name, edge_function_definition)] - .into_iter() - .collect(), - }; - // TODO(alexkirsz) Add the manifest. - } - fn create_app_paths_manifest( node_root: Vc, original_name: &str, @@ -676,37 +648,109 @@ impl AppEndpoint { let endpoint_output = match app_entry.config.await?.runtime.unwrap_or_default() { NextRuntime::Edge => { + // create edge chunks let chunking_context = this.app_project.project().edge_rsc_chunking_context(); + let mut evaluatable_assets = this + .app_project + .edge_rsc_runtime_entries() + .await? + .clone_value(); + let Some(evaluatable) = Vc::try_resolve_sidecast(app_entry.rsc_entry).await? else { + bail!("Entry module must be evaluatable"); + }; + evaluatable_assets.push(evaluatable); let files = chunking_context.evaluated_chunk_group( app_entry .rsc_entry .as_root_chunk(Vc::upcast(chunking_context)), - this.app_project.edge_rsc_runtime_entries(), - ); - // TODO concatenation is not good, we should use all files once next.js supports - // that output_assets.extend(files.await?.iter().copied()); - let file = concatenate_output_assets( - server_path.join(format!( - "app/{original_name}.js", - original_name = app_entry.original_name - )), - files, + Vc::cell(evaluatable_assets), ); - output_assets.push(file); + output_assets.extend(files.await?.iter().copied()); - let app_paths_manifest_output = create_app_paths_manifest( - node_root, - &app_entry.original_name, - server_path + let node_root_value = node_root.await?; + let files_paths_from_root = files + .await? + .iter() + .map(move |&file| { + let node_root_value = node_root_value.clone(); + async move { + Ok(node_root_value + .get_path_to(&*file.ident().path().await?) + .map(|path| path.to_string())) + } + }) + .try_flat_join() + .await?; + + println!("files_paths_from_root: {:?}", files_paths_from_root); + + let server_path_value = server_path.await?; + let files_paths_from_server = files + .await? + .iter() + .map(move |&file| { + let server_path_value = server_path_value.clone(); + async move { + Ok(server_path_value + .get_path_to(&*file.ident().path().await?) + .map(|path| path.to_string())) + } + }) + .try_flat_join() + .await?; + let base_file = files_paths_from_server[0].to_string(); + + println!("files_paths_from_server: {:?}", files_paths_from_server); + + // create middleware manifest + // TODO(alexkirsz) This should be shared with next build. + let named_regex = get_named_middleware_regex(&app_entry.pathname); + let matchers = MiddlewareMatcher { + regexp: named_regex, + original_source: app_entry.pathname.clone(), + ..Default::default() + }; + let edge_function_definition = EdgeFunctionDefinition { + files: files_paths_from_root, + name: "".to_string(), + page: app_entry.original_name.clone(), + regions: app_entry + .config .await? - .get_path_to(&*file.ident().path().await?) - .expect("edge bundle path should be within app paths manifest directory") - .to_string(), - )?; + .preferred_region + .clone() + .map(|region| Regions::Single(region)), + matchers: vec![matchers], + ..Default::default() + }; + let middleware_manifest_v2 = MiddlewaresManifestV2 { + sorted_middleware: vec![app_entry.original_name.clone()], + middleware: Default::default(), + functions: [(app_entry.original_name.clone(), edge_function_definition)] + .into_iter() + .collect(), + }; + let middleware_manifest_v2 = Vc::upcast(VirtualOutputAsset::new( + node_root.join(format!( + "server/app{original_name}/middleware-manifest.json", + original_name = app_entry.original_name + )), + AssetContent::file( + FileContent::Content(File::from(serde_json::to_string_pretty( + &middleware_manifest_v2, + )?)) + .cell(), + ), + )); + output_assets.push(middleware_manifest_v2); + + // create app paths manifest + let app_paths_manifest_output = + create_app_paths_manifest(node_root, &app_entry.original_name, base_file)?; output_assets.push(app_paths_manifest_output); AppEndpointOutput::Edge { - file, + files, output_assets: Vc::cell(output_assets), } } @@ -813,13 +857,20 @@ impl Endpoint for AppEndpoint { server_paths, }, AppEndpointOutput::Edge { - file, + files, output_assets: _, } => WrittenEndpoint::Edge { - files: vec![node_root_ref - .get_path_to(&*file.ident().path().await?) - .context("edge chunk file path must be inside the node root")? - .to_string()], + files: files + .await? + .iter() + .map(|&file| async move { + Ok(node_root_ref + .get_path_to(&*file.ident().path().await?) + .context("edge chunk file path must be inside the node root")? + .to_string()) + }) + .try_join() + .await?, global_var_name: "TODO".to_string(), server_paths, }, @@ -841,7 +892,7 @@ enum AppEndpointOutput { output_assets: Vc, }, Edge { - file: Vc>, + files: Vc, output_assets: Vc, }, } @@ -856,7 +907,7 @@ impl AppEndpointOutput { output_assets, } => output_assets, AppEndpointOutput::Edge { - file: _, + files: _, output_assets, } => output_assets, } diff --git a/packages/next-swc/crates/next-core/src/lib.rs b/packages/next-swc/crates/next-core/src/lib.rs index 43b05dab0a8a1..11ff384c5cbc9 100644 --- a/packages/next-swc/crates/next-core/src/lib.rs +++ b/packages/next-swc/crates/next-core/src/lib.rs @@ -27,7 +27,7 @@ mod next_client_component; pub mod next_client_reference; pub mod next_config; pub mod next_dynamic; -mod next_edge; +pub mod next_edge; mod next_font; pub mod next_image; mod next_import_map; diff --git a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs index 5082968107d70..cb96dd2a63090 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs @@ -1,5 +1,8 @@ +use std::io::Write; + use anyhow::{bail, Result}; use indexmap::indexmap; +use indoc::writedoc; use turbo_tasks::{Value, ValueToString, Vc}; use turbopack_binding::{ turbo::tasks_fs::{rope::RopeBuilder, File, FileSystemPath}, @@ -7,9 +10,7 @@ use turbopack_binding::{ core::{ asset::AssetContent, context::AssetContext, - reference_type::{ - EcmaScriptModulesReferenceSubType, EntryReferenceSubType, ReferenceType, - }, + reference_type::{EntryReferenceSubType, ReferenceType}, source::Source, virtual_source::VirtualSource, }, @@ -34,14 +35,13 @@ pub async fn get_app_route_entry( original_name: String, project_root: Vc, ) -> Result> { - let config = parse_segment_config_from_source( - nodejs_context.process( - source, - Value::new(ReferenceType::Entry(EntryReferenceSubType::AppRoute)), - ), + let userland_module = nodejs_context.process( source, + Value::new(ReferenceType::Entry(EntryReferenceSubType::AppRoute)), ); - let context = if matches!(config.await?.runtime, Some(NextRuntime::Edge)) { + let config = parse_segment_config_from_source(userland_module, source); + let is_edge = matches!(config.await?.runtime, Some(NextRuntime::Edge)); + let context = if is_edge { edge_context } else { nodejs_context @@ -102,22 +102,49 @@ pub async fn get_app_route_entry( let virtual_source = VirtualSource::new(template_path, AssetContent::file(file.into())); - let entry = context.process( - source, - Value::new(ReferenceType::EcmaScriptModules( - EcmaScriptModulesReferenceSubType::Undefined, - )), - ); - let inner_assets = indexmap! { - "VAR_USERLAND".to_string() => entry + "VAR_USERLAND".to_string() => userland_module }; - let rsc_entry = context.process( + let mut rsc_entry = context.process( Vc::upcast(virtual_source), Value::new(ReferenceType::Internal(Vc::cell(inner_assets))), ); + if is_edge { + let mut source = RopeBuilder::default(); + writedoc!( + source, + r#" + import {{ EdgeRouteModuleWrapper }} from 'next/dist/esm/server/web/edge-route-module-wrapper' + import * as module from "MODULE" + + export const ComponentMod = module + + console.log("loaded edge route module") + self._ENTRIES ||= {{}} + self._ENTRIES.middleware_ = {{ + ComponentMod: module, + default: EdgeRouteModuleWrapper.wrap(module.routeModule), + }} + "# + )?; + let file = File::from(source.build()); + // TODO(alexkirsz) Figure out how to name this virtual asset. + let virtual_source = VirtualSource::new( + project_root.join("edge-wrapper.js".to_string()), + AssetContent::file(file.into()), + ); + let inner_assets = indexmap! { + "MODULE".to_string() => rsc_entry + }; + + rsc_entry = context.process( + Vc::upcast(virtual_source), + Value::new(ReferenceType::Internal(Vc::cell(inner_assets))), + ); + } + let Some(rsc_entry) = Vc::try_resolve_downcast::>(rsc_entry).await? else { diff --git a/packages/next-swc/crates/next-core/src/next_edge/context.rs b/packages/next-swc/crates/next-core/src/next_edge/context.rs index 6b535b68b22b4..567669831e716 100644 --- a/packages/next-swc/crates/next-core/src/next_edge/context.rs +++ b/packages/next-swc/crates/next-core/src/next_edge/context.rs @@ -141,8 +141,8 @@ pub fn get_edge_chunking_context( Vc::upcast( DevChunkingContext::builder( project_path, - node_root.join("edge".to_string()), - node_root.join("edge/chunks".to_string()), + node_root.join("server/edge".to_string()), + node_root.join("server/edge/chunks".to_string()), get_client_assets_path(client_root), environment, ) diff --git a/packages/next-swc/crates/next-core/src/next_edge/mod.rs b/packages/next-swc/crates/next-core/src/next_edge/mod.rs index 8f8bcfae99304..1a3e483a16528 100644 --- a/packages/next-swc/crates/next-core/src/next_edge/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_edge/mod.rs @@ -1,4 +1,4 @@ pub mod context; pub mod page_transition; -pub(crate) mod route_regex; +pub mod route_regex; pub mod route_transition; diff --git a/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs b/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs index 405ccd902f6e1..e81f7480f7bff 100644 --- a/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs +++ b/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs @@ -11,10 +11,10 @@ use std::collections::HashMap; -use lazy_static::lazy_static; +use once_cell::sync::Lazy; use regex::Regex; -const INTERCEPTION_ROUTE_MARKERS: [&str; 0] = []; // Filler value +const INTERCEPTION_ROUTE_MARKERS: [&str; 4] = ["(..)(..)", "(.)", "(..)", "(...)"]; const NEXT_QUERY_PARAM_PREFIX: &str = "nxtP"; const NEXT_INTERCEPTION_MARKER_PREFIX: &str = "nxtI"; @@ -28,19 +28,25 @@ pub struct Group { #[derive(Debug)] pub struct RouteRegex { pub groups: HashMap, - pub re: Regex, + pub regex: String, } #[derive(Debug)] pub struct NamedRouteRegex { pub regex: RouteRegex, - pub named_regex: Regex, + pub named_regex: String, pub route_keys: HashMap, } #[derive(Debug)] pub struct NamedMiddlewareRegex { - pub named_regex: Regex, + pub named_regex: String, +} + +struct ParsedParameter { + key: String, + repeat: bool, + optional: bool, } /// Parses a given parameter from a route to a data structure that can be used @@ -49,30 +55,32 @@ pub struct NamedMiddlewareRegex { /// - `...slug` -> `{ key: 'slug', repeat: true, optional: false }` /// - `[foo]` -> `{ key: 'foo', repeat: false, optional: true }` /// - `bar` -> `{ key: 'bar', repeat: false, optional: false }` -fn parse_parameter(param: &str) -> (String, bool, bool) { - let mut param = param.to_string(); - let optional = param.starts_with('[') && param.ends_with(']'); +fn parse_parameter(param: &str) -> ParsedParameter { + let mut key = param.to_string(); + let optional = key.starts_with('[') && key.ends_with(']'); if optional { - param = param[1..param.len() - 1].to_string(); + key = key[1..key.len() - 1].to_string(); } - let repeat = param.starts_with("..."); + let repeat = key.starts_with("..."); if repeat { - param = param[3..].to_string(); + key = key[3..].to_string(); + } + ParsedParameter { + key, + repeat, + optional, } - (param, repeat, optional) } fn escape_string_regexp(segment: &str) -> String { regex::escape(segment) } -fn remove_trailing_slash(route: &str) -> String { - route.trim_end_matches('/').to_string() +fn remove_trailing_slash(route: &str) -> &str { + route.trim_end_matches('/') } -lazy_static! { - static ref PARAM_MATCH_REGEX: Regex = Regex::new(r"\[((?:\[.*\])|.+)\]").unwrap(); -} +static PARAM_MATCH_REGEX: Lazy = Lazy::new(|| Regex::new(r"\[((?:\[.*\])|.+)\]").unwrap()); fn get_parametrized_route(route: &str) -> (String, HashMap) { let segments: Vec<&str> = remove_trailing_slash(route)[1..].split('/').collect(); @@ -83,26 +91,17 @@ fn get_parametrized_route(route: &str) -> (String, HashMap) { .map(|segment| { let marker_match = INTERCEPTION_ROUTE_MARKERS .iter() - .find(|m| segment.starts_with(m)); + .find(|&&m| segment.starts_with(m)) + .copied(); let param_matches = PARAM_MATCH_REGEX.captures(segment); - if let Some(marker) = marker_match { - if let Some(matches) = param_matches { - let (key, optional, repeat) = parse_parameter(&matches[1]); - groups.insert( - key.clone(), - Group { - pos: group_index, - repeat, - optional, - }, - ); - group_index += 1; - return format!("/{}([^/]+?)", escape_string_regexp(marker)); - } - } else if let Some(matches) = param_matches { - let (key, optional, repeat) = parse_parameter(&matches[1]); + if let Some(matches) = param_matches { + let ParsedParameter { + key, + optional, + repeat, + } = parse_parameter(&matches[1]); groups.insert( - key.clone(), + key, Group { pos: group_index, repeat, @@ -110,16 +109,17 @@ fn get_parametrized_route(route: &str) -> (String, HashMap) { }, ); group_index += 1; - return if repeat { - if optional { - "(?:/(.+?))?" - } else { - "/(.+?)" - } + if let Some(marker) = marker_match { + return format!("/{}([^/]+?)", escape_string_regexp(marker)); } else { - "/([^/]+?)" + return match (repeat, optional) { + (true, true) => "(?:/(.+?))?", + (true, false) => "/(.+?)", + (false, true) => "(?:/([^/]+?))?", + (false, false) => "/([^/]+?)", + } + .to_string(); } - .to_string(); } format!("/{}", escape_string_regexp(segment)) }) @@ -134,14 +134,14 @@ fn get_parametrized_route(route: &str) -> (String, HashMap) { pub fn get_route_regex(normalized_route: &str) -> RouteRegex { let (parameterized_route, groups) = get_parametrized_route(normalized_route); RouteRegex { - re: Regex::new(&format!("^{}(?:/)?$", parameterized_route)).unwrap(), + regex: format!("^{}(?:/)?$", parameterized_route), groups, } } /// Builds a function to generate a minimal routeKey using only a-z and minimal /// number of characters. -fn build_get_safe_route_key() -> Box String> { +fn build_get_safe_route_key() -> impl FnMut() -> String { let mut route_key_char_code = 97; let mut route_key_char_length = 1; Box::new(move || { @@ -159,12 +159,16 @@ fn build_get_safe_route_key() -> Box String> { } fn get_safe_key_from_segment( + get_safe_route_key: &mut impl FnMut() -> String, segment: &str, route_keys: &mut HashMap, - key_prefix: Option, + key_prefix: Option<&'static str>, ) -> String { - let mut get_safe_route_key = build_get_safe_route_key(); - let (key, optional, repeat) = parse_parameter(segment); + let ParsedParameter { + key, + optional, + repeat, + } = parse_parameter(segment); // replace any non-word characters since they can break // the named regex @@ -190,14 +194,11 @@ fn get_safe_key_from_segment( } else { route_keys.insert(cleaned_key.clone(), key); } - if repeat { - if optional { - format!(r"(?:/(?P<{}>.+?))?", cleaned_key) - } else { - format!(r"/(?P<{}>.+?)", cleaned_key) - } - } else { - format!(r"/(?P<{}>[^/]+?)", cleaned_key) + match (repeat, optional) { + (true, true) => format!(r"(?:/(?P<{}>.+?))?", cleaned_key), + (true, false) => format!(r"/(?P<{}>.+?)", cleaned_key), + (false, true) => format!(r"(?:/(?P<{}>[^/]+?))?", cleaned_key), + (false, false) => format!(r"/(?P<{}>[^/]+?)", cleaned_key), } } @@ -206,26 +207,33 @@ fn get_named_parametrized_route( prefix_route_keys: bool, ) -> (String, HashMap) { let segments: Vec<&str> = remove_trailing_slash(route)[1..].split('/').collect(); + let get_safe_route_key = &mut build_get_safe_route_key(); let mut route_keys: HashMap = HashMap::new(); let parameterized_route = segments .iter() .map(|segment| { - let marker_match = INTERCEPTION_ROUTE_MARKERS - .iter() - .find(|m| segment.starts_with(m)); + let key_prefix = if prefix_route_keys { + let has_interception_marker = INTERCEPTION_ROUTE_MARKERS + .iter() + .any(|&m| segment.starts_with(m)); + if has_interception_marker { + Some(NEXT_INTERCEPTION_MARKER_PREFIX) + } else { + Some(NEXT_QUERY_PARAM_PREFIX) + } + } else { + None + }; let param_matches = Regex::new(r"\[((?:\[.*\])|.+)\]") .unwrap() .captures(segment); - if let Some(marker) = marker_match { - if let Some(matches) = param_matches { - return get_safe_key_from_segment( - &matches[1], - &mut route_keys, - Some(escape_string_regexp(marker)), - ); - } - } else if let Some(matches) = param_matches { - return get_safe_key_from_segment(&matches[1], &mut route_keys, None); + if let Some(matches) = param_matches { + return get_safe_key_from_segment( + get_safe_route_key, + &matches[1], + &mut route_keys, + key_prefix, + ); } format!("/{}", escape_string_regexp(segment)) }) @@ -245,16 +253,14 @@ pub fn get_named_route_regex(normalized_route: &str) -> NamedRouteRegex { let regex = get_route_regex(normalized_route); NamedRouteRegex { regex, - named_regex: Regex::new(&format!("^{}(?:/)?$", parameterized_route)).unwrap(), + named_regex: format!("^{}(?:/)?$", parameterized_route), route_keys, } } /// Generates a named regexp. /// This is intended to be using for build time only. -pub fn get_named_middleware_regex(normalized_route: &str) -> NamedMiddlewareRegex { +pub fn get_named_middleware_regex(normalized_route: &str) -> String { let (parameterized_route, _route_keys) = get_named_parametrized_route(normalized_route, true); - NamedMiddlewareRegex { - named_regex: Regex::new(&format!("^{}(?:/)?$", parameterized_route)).unwrap(), - } + format!("^{}(?:/)?$", parameterized_route) } diff --git a/packages/next-swc/crates/next-core/src/next_manifests/mod.rs b/packages/next-swc/crates/next-core/src/next_manifests/mod.rs index 82bc7336bdc68..fdbdce042d457 100644 --- a/packages/next-swc/crates/next-core/src/next_manifests/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_manifests/mod.rs @@ -67,14 +67,14 @@ pub enum RouteHas { #[derive(Serialize, Default, Debug)] #[serde(rename_all = "camelCase")] pub struct MiddlewareMatcher { - regexp: String, + pub regexp: String, #[serde(skip_serializing_if = "bool_is_true")] - locale: bool, + pub locale: bool, #[serde(skip_serializing_if = "Option::is_none")] - has: Option>, + pub has: Option>, #[serde(skip_serializing_if = "Option::is_none")] - missing: Option>, - original_source: String, + pub missing: Option>, + pub original_source: String, } fn bool_is_true(b: &bool) -> bool { @@ -83,18 +83,18 @@ fn bool_is_true(b: &bool) -> bool { #[derive(Serialize, Default, Debug)] pub struct EdgeFunctionDefinition { - files: Vec, - name: String, - page: String, - matchers: Vec, + pub files: Vec, + pub name: String, + pub page: String, + pub matchers: Vec, // TODO: AssetBinding[] #[serde(skip_serializing_if = "Option::is_none")] - wasm: Option>, + pub wasm: Option>, // TODO: AssetBinding[] #[serde(skip_serializing_if = "Option::is_none")] - assets: Option>, + pub assets: Option>, #[serde(skip_serializing_if = "Option::is_none")] - regions: Option, + pub regions: Option, } #[derive(Serialize, Debug)] diff --git a/packages/next/src/server/lib/router-utils/setup-dev.ts b/packages/next/src/server/lib/router-utils/setup-dev.ts index e4831d68c4c12..a1c9e40a6b23d 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev.ts @@ -48,6 +48,7 @@ import { COMPILER_NAMES, DEV_CLIENT_PAGES_MANIFEST, DEV_MIDDLEWARE_MANIFEST, + MIDDLEWARE_MANIFEST, NEXT_FONT_MANIFEST, PAGES_MANIFEST, PHASE_DEVELOPMENT_SERVER, @@ -78,6 +79,7 @@ import { PagesManifest } from '../../../build/webpack/plugins/pages-manifest-plu import { AppBuildManifest } from '../../../build/webpack/plugins/app-build-manifest-plugin' import { PageNotFoundError } from '../../../shared/lib/utils' import { srcEmptySsgManifest } from '../../../build/webpack/plugins/build-manifest-plugin' +import { MiddlewareManifest } from '../../../build/webpack/plugins/middleware-plugin' type SetupOpts = { dir: string @@ -203,6 +205,7 @@ async function startWatcher(opts: SetupOpts) { const appBuildManifests = new Map() const pagesManifests = new Map() const appPathsManifests = new Map() + const middlewareManifests = new Map() function mergeBuildManifests(manifests: Iterable) { const manifest: Partial & Pick = { @@ -245,6 +248,21 @@ async function startWatcher(opts: SetupOpts) { return manifest } + function mergeMiddlewareManifests( + manifests: Iterable + ): MiddlewareManifest { + const manifest: MiddlewareManifest = { + version: 2, + middleware: {}, + sortedMiddleware: [], + functions: {}, + } + for (const m of manifests) { + Object.assign(manifest.functions, m.functions) + } + return manifest + } + async function processResult( result: TurbopackResult | undefined ): Promise | undefined> { @@ -352,6 +370,9 @@ async function startWatcher(opts: SetupOpts) { } async function writeMiddlewareManifest(): Promise { + const middlewareManifest = mergeMiddlewareManifests( + middlewareManifests.values() + ) const middlewareManifestPath = path.join( distDir, 'server/middleware-manifest.json' @@ -359,16 +380,7 @@ async function startWatcher(opts: SetupOpts) { await clearCache(middlewareManifestPath) await writeFile( middlewareManifestPath, - JSON.stringify( - { - sortedMiddleware: [], - middleware: {}, - functions: {}, - version: 2, - }, - null, - 2 - ), + JSON.stringify(middlewareManifest, null, 2), 'utf-8' ) } @@ -499,6 +511,22 @@ async function startWatcher(opts: SetupOpts) { ) } + async function loadMiddlewareManifest( + pageName: string, + isApp: boolean = false, + isRoute: boolean = false + ): Promise { + middlewareManifests.set( + pageName, + await loadPartialManifest( + MIDDLEWARE_MANIFEST, + pageName, + isApp, + isRoute + ) + ) + } + if (page === '/_error') { await processResult(await globalEntries.app?.writeToDisk()) await loadBuildManifest('_app') @@ -594,13 +622,18 @@ async function startWatcher(opts: SetupOpts) { break } case 'app-route': { - await processResult(await route.endpoint.writeToDisk()) + const type = ( + await processResult(await route.endpoint.writeToDisk()) + )?.type await loadAppPathManifest(page, true) + if (type === 'edge') + await loadMiddlewareManifest(page, true, true) await writeAppBuildManifest() await writeAppPathsManifest() await writeMiddlewareManifest() + if (type === 'edge') await writeMiddlewareManifest() await writeOtherManifests() break diff --git a/packages/next/src/shared/lib/router/utils/route-regex.ts b/packages/next/src/shared/lib/router/utils/route-regex.ts index 4b1714b6a9167..848de809a7f92 100644 --- a/packages/next/src/shared/lib/router/utils/route-regex.ts +++ b/packages/next/src/shared/lib/router/utils/route-regex.ts @@ -102,16 +102,16 @@ function buildGetSafeRouteKey() { } function getSafeKeyFromSegment({ + getSafeRouteKey, segment, routeKeys, keyPrefix, }: { + getSafeRouteKey: () => string segment: string routeKeys: Record keyPrefix?: string }) { - const getSafeRouteKey = buildGetSafeRouteKey() - const { key, optional, repeat } = parseParameter(segment) // replace any non-word characters since they can break @@ -151,6 +151,7 @@ function getSafeKeyFromSegment({ function getNamedParametrizedRoute(route: string, prefixRouteKeys: boolean) { const segments = removeTrailingSlash(route).slice(1).split('/') + const getSafeRouteKey = buildGetSafeRouteKey() const routeKeys: { [named: string]: string } = {} return { namedParameterizedRoute: segments @@ -162,6 +163,7 @@ function getNamedParametrizedRoute(route: string, prefixRouteKeys: boolean) { if (hasInterceptionMarker && paramMatches) { return getSafeKeyFromSegment({ + getSafeRouteKey, segment: paramMatches[1], routeKeys, keyPrefix: prefixRouteKeys @@ -170,6 +172,7 @@ function getNamedParametrizedRoute(route: string, prefixRouteKeys: boolean) { }) } else if (paramMatches) { return getSafeKeyFromSegment({ + getSafeRouteKey, segment: paramMatches[1], routeKeys, keyPrefix: prefixRouteKeys ? NEXT_QUERY_PARAM_PREFIX : undefined, From 118af11f63b85005170132dc81f6bb84370a0f05 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Mon, 31 Jul 2023 15:19:39 +0000 Subject: [PATCH 03/15] finalize support for edge routes --- packages/next-swc/crates/next-api/src/app.rs | 6 +- .../src/next_app/app_favicon_entry.rs | 2 +- .../next-core/src/next_app/app_page_entry.rs | 7 +- .../next-core/src/next_app/app_route_entry.rs | 72 +++++++++++-------- .../next/src/server/app-render/app-render.tsx | 1 + 5 files changed, 50 insertions(+), 38 deletions(-) diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index 3e78345b24122..933db84882375 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -682,8 +682,6 @@ impl AppEndpoint { .try_flat_join() .await?; - println!("files_paths_from_root: {:?}", files_paths_from_root); - let server_path_value = server_path.await?; let files_paths_from_server = files .await? @@ -700,8 +698,6 @@ impl AppEndpoint { .await?; let base_file = files_paths_from_server[0].to_string(); - println!("files_paths_from_server: {:?}", files_paths_from_server); - // create middleware manifest // TODO(alexkirsz) This should be shared with next build. let named_regex = get_named_middleware_regex(&app_entry.pathname); @@ -712,7 +708,7 @@ impl AppEndpoint { }; let edge_function_definition = EdgeFunctionDefinition { files: files_paths_from_root, - name: "".to_string(), + name: app_entry.original_name.to_string(), page: app_entry.original_name.clone(), regions: app_entry .config diff --git a/packages/next-swc/crates/next-core/src/next_app/app_favicon_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_favicon_entry.rs index 2a10c97cbd000..0f4df5eb01f39 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_favicon_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_favicon_entry.rs @@ -77,7 +77,7 @@ pub async fn get_app_route_favicon_entry( let file = File::from(code.build()); let source = // TODO(alexkirsz) Figure out how to name this virtual source. - VirtualSource::new(project_root.join("todo.tsx".to_string()), AssetContent::file(file.into())); + VirtualSource::new(project_root.join("favicon-entry.tsx".to_string()), AssetContent::file(file.into())); Ok(get_app_route_entry( nodejs_context, diff --git a/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs index e889422c60a3a..3b79382f3e52f 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs @@ -37,7 +37,8 @@ pub async fn get_app_page_entry( project_root: Vc, ) -> Result> { let config = parse_segment_config_from_loader_tree(loader_tree, Vc::upcast(nodejs_context)); - let context = if matches!(config.await?.runtime, Some(NextRuntime::Edge)) { + let is_edge = matches!(config.await?.runtime, Some(NextRuntime::Edge)); + let context = if is_edge { edge_context } else { nodejs_context @@ -139,6 +140,10 @@ pub async fn get_app_page_entry( Value::new(ReferenceType::Internal(Vc::cell(inner_assets))), ); + if is_edge { + todo!("edge pages are not supported yet") + } + let Some(rsc_entry) = Vc::try_resolve_downcast::>(rsc_entry).await? else { diff --git a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs index cb96dd2a63090..cb378fbfc9fc9 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs @@ -112,37 +112,7 @@ pub async fn get_app_route_entry( ); if is_edge { - let mut source = RopeBuilder::default(); - writedoc!( - source, - r#" - import {{ EdgeRouteModuleWrapper }} from 'next/dist/esm/server/web/edge-route-module-wrapper' - import * as module from "MODULE" - - export const ComponentMod = module - - console.log("loaded edge route module") - self._ENTRIES ||= {{}} - self._ENTRIES.middleware_ = {{ - ComponentMod: module, - default: EdgeRouteModuleWrapper.wrap(module.routeModule), - }} - "# - )?; - let file = File::from(source.build()); - // TODO(alexkirsz) Figure out how to name this virtual asset. - let virtual_source = VirtualSource::new( - project_root.join("edge-wrapper.js".to_string()), - AssetContent::file(file.into()), - ); - let inner_assets = indexmap! { - "MODULE".to_string() => rsc_entry - }; - - rsc_entry = context.process( - Vc::upcast(virtual_source), - Value::new(ReferenceType::Internal(Vc::cell(inner_assets))), - ); + rsc_entry = wrap_edge_entry(context, project_root, rsc_entry, original_name.clone()); } let Some(rsc_entry) = @@ -160,6 +130,46 @@ pub async fn get_app_route_entry( .cell()) } +#[turbo_tasks::function] +pub async fn wrap_edge_entry( + context: Vc, + project_root: Vc, + entry: Vc>, + original_name: String, +) -> Result>> { + let mut source = RopeBuilder::default(); + writedoc!( + source, + r#" + import {{ EdgeRouteModuleWrapper }} from 'next/dist/esm/server/web/edge-route-module-wrapper' + import * as module from "MODULE" + + export const ComponentMod = module + + self._ENTRIES ||= {{}} + self._ENTRIES[{}] = {{ + ComponentMod: module, + default: EdgeRouteModuleWrapper.wrap(module.routeModule), + }} + "#, + StringifyJs(&format_args!("middleware_{}", original_name)) + )?; + let file = File::from(source.build()); + // TODO(alexkirsz) Figure out how to name this virtual asset. + let virtual_source = VirtualSource::new( + project_root.join("edge-wrapper.js".to_string()), + AssetContent::file(file.into()), + ); + let inner_assets = indexmap! { + "MODULE".to_string() => entry + }; + + Ok(context.process( + Vc::upcast(virtual_source), + Value::new(ReferenceType::Internal(Vc::cell(inner_assets))), + )) +} + fn get_original_route_name(pathname: &str) -> String { match pathname { "/" => "/route".to_string(), diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index 30439eebec15c..ad90a16546664 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -252,6 +252,7 @@ export async function renderToHTMLOrFlight( allCapturedErrors, }) + console.log(ComponentMod) patchFetch(ComponentMod) /** * Rules of Static & Dynamic HTML: From a2e395ab3359e295a3c7d37dca566c224655bd33 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Wed, 2 Aug 2023 15:58:51 +0000 Subject: [PATCH 04/15] fixup --- .../next-swc/crates/next-core/src/next_app/app_route_entry.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs index cb378fbfc9fc9..d4a075bd1e388 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs @@ -10,6 +10,7 @@ use turbopack_binding::{ core::{ asset::AssetContent, context::AssetContext, + module::Module, reference_type::{EntryReferenceSubType, ReferenceType}, source::Source, virtual_source::VirtualSource, From 4710652952c51e6249d7c360222ebe5a3e5204d2 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Wed, 2 Aug 2023 17:37:16 +0000 Subject: [PATCH 05/15] add test case --- .../app-simple-routes.test.ts | 30 +++++++++++++++++++ .../app/api/edge.json/route.ts | 9 ++++++ .../app/api/node.json/route.ts | 7 +++++ .../app-dir/app-simple-routes/next.config.js | 10 +++++++ test/turbopack-tests-manifest.js | 2 ++ 5 files changed, 58 insertions(+) create mode 100644 test/e2e/app-dir/app-simple-routes/app-simple-routes.test.ts create mode 100644 test/e2e/app-dir/app-simple-routes/app/api/edge.json/route.ts create mode 100644 test/e2e/app-dir/app-simple-routes/app/api/node.json/route.ts create mode 100644 test/e2e/app-dir/app-simple-routes/next.config.js diff --git a/test/e2e/app-dir/app-simple-routes/app-simple-routes.test.ts b/test/e2e/app-dir/app-simple-routes/app-simple-routes.test.ts new file mode 100644 index 0000000000000..9a08307c55d3e --- /dev/null +++ b/test/e2e/app-dir/app-simple-routes/app-simple-routes.test.ts @@ -0,0 +1,30 @@ +import { createNextDescribe } from 'e2e-utils' +import { check } from 'next-test-utils' +import { Readable } from 'stream' + +const bathPath = process.env.BASE_PATH ?? '' + +createNextDescribe( + 'app-simple-routes', + { + files: __dirname, + }, + ({ next, isNextDeploy, isNextDev, isNextStart }) => { + describe('works with simple routes', () => { + it('renders a node route', async () => { + expect( + JSON.parse(await next.render(bathPath + '/api/node.json')) + ).toEqual({ + pathname: '/api/node.json', + }) + }) + it('renders a edge route', async () => { + expect( + JSON.parse(await next.render(bathPath + '/api/edge.json')) + ).toEqual({ + pathname: '/api/edge.json', + }) + }) + }) + } +) diff --git a/test/e2e/app-dir/app-simple-routes/app/api/edge.json/route.ts b/test/e2e/app-dir/app-simple-routes/app/api/edge.json/route.ts new file mode 100644 index 0000000000000..3ab412792ad94 --- /dev/null +++ b/test/e2e/app-dir/app-simple-routes/app/api/edge.json/route.ts @@ -0,0 +1,9 @@ +import { NextRequest, NextResponse } from 'next/server' + +export const GET = (req: NextRequest) => { + return NextResponse.json({ + pathname: req.nextUrl.pathname, + }) +} + +export const runtime = 'edge' diff --git a/test/e2e/app-dir/app-simple-routes/app/api/node.json/route.ts b/test/e2e/app-dir/app-simple-routes/app/api/node.json/route.ts new file mode 100644 index 0000000000000..3dfb293bcfa4b --- /dev/null +++ b/test/e2e/app-dir/app-simple-routes/app/api/node.json/route.ts @@ -0,0 +1,7 @@ +import { NextRequest, NextResponse } from 'next/server' + +export const GET = (req: NextRequest) => { + return NextResponse.json({ + pathname: req.nextUrl.pathname, + }) +} diff --git a/test/e2e/app-dir/app-simple-routes/next.config.js b/test/e2e/app-dir/app-simple-routes/next.config.js new file mode 100644 index 0000000000000..d54bad4c24cbe --- /dev/null +++ b/test/e2e/app-dir/app-simple-routes/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const config = { + typescript: { + ignoreBuildErrors: true, + }, +} + +module.exports = config diff --git a/test/turbopack-tests-manifest.js b/test/turbopack-tests-manifest.js index e6d94a4f01838..cffb7b230ce2f 100644 --- a/test/turbopack-tests-manifest.js +++ b/test/turbopack-tests-manifest.js @@ -3,6 +3,8 @@ // be enabled here const enabledTests = [ 'test/development/api-cors-with-rewrite/index.test.ts', + 'test/e2e/app-dir/app-rendering/rendering.test.ts', + 'test/e2e/app-dir/app-simple-routes/app-simple-routes.test.ts', 'test/integration/bigint/test/index.test.js', ] From e0d0280c32f6f237358136403070b11c5d093287 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Wed, 2 Aug 2023 19:22:36 +0000 Subject: [PATCH 06/15] fix edge import map and merge issues --- .../next-core/src/next_app/app_route_entry.rs | 17 +++++++---- .../crates/next-core/src/next_import_map.rs | 29 ++++++++++++------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs index d4a075bd1e388..b77c77c84e5a8 100644 --- a/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs +++ b/packages/next-swc/crates/next-core/src/next_app/app_route_entry.rs @@ -36,11 +36,13 @@ pub async fn get_app_route_entry( original_name: String, project_root: Vc, ) -> Result> { - let userland_module = nodejs_context.process( + let config = parse_segment_config_from_source( + nodejs_context.process( + source, + Value::new(ReferenceType::Entry(EntryReferenceSubType::AppRoute)), + ), source, - Value::new(ReferenceType::Entry(EntryReferenceSubType::AppRoute)), ); - let config = parse_segment_config_from_source(userland_module, source); let is_edge = matches!(config.await?.runtime, Some(NextRuntime::Edge)); let context = if is_edge { edge_context @@ -103,6 +105,11 @@ pub async fn get_app_route_entry( let virtual_source = VirtualSource::new(template_path, AssetContent::file(file.into())); + let userland_module = context.process( + source, + Value::new(ReferenceType::Entry(EntryReferenceSubType::AppRoute)), + ); + let inner_assets = indexmap! { "VAR_USERLAND".to_string() => userland_module }; @@ -113,7 +120,7 @@ pub async fn get_app_route_entry( ); if is_edge { - rsc_entry = wrap_edge_entry(context, project_root, rsc_entry, original_name.clone()); + rsc_entry = wrap_edge_entry(context, project_root, rsc_entry, original_page_name.clone()); } let Some(rsc_entry) = @@ -145,8 +152,6 @@ pub async fn wrap_edge_entry( import {{ EdgeRouteModuleWrapper }} from 'next/dist/esm/server/web/edge-route-module-wrapper' import * as module from "MODULE" - export const ComponentMod = module - self._ENTRIES ||= {{}} self._ENTRIES[{}] = {{ ComponentMod: module, diff --git a/packages/next-swc/crates/next-core/src/next_import_map.rs b/packages/next-swc/crates/next-core/src/next_import_map.rs index e099e880944ba..b19441600bfcd 100644 --- a/packages/next-swc/crates/next-core/src/next_import_map.rs +++ b/packages/next-swc/crates/next-core/src/next_import_map.rs @@ -211,7 +211,7 @@ pub async fn get_next_server_import_map( let ty = ty.into_value(); - insert_next_server_special_aliases(&mut import_map, ty, mode).await?; + insert_next_server_special_aliases(&mut import_map, ty, mode, false).await?; let external = ImportMapping::External(None).cell(); match ty { @@ -283,7 +283,7 @@ pub async fn get_next_edge_import_map( let ty = ty.into_value(); - insert_next_server_special_aliases(&mut import_map, ty, mode).await?; + insert_next_server_special_aliases(&mut import_map, ty, mode, true).await?; match ty { ServerContextType::Pages { .. } | ServerContextType::PagesData { .. } => {} @@ -360,21 +360,29 @@ pub async fn insert_next_server_special_aliases( import_map: &mut ImportMap, ty: ServerContextType, mode: NextMode, + is_edge: bool, ) -> Result<()> { + let external_if_node = move |context_dir: Vc, request: &str| { + if is_edge { + request_to_import_mapping(context_dir, request) + } else { + external_request_to_import_mapping(request) + } + }; match (mode, ty) { (_, ServerContextType::Pages { pages_dir }) => { import_map.insert_exact_alias( "@opentelemetry/api", // TODO(WEB-625) this actually need to prefer the local version of // @opentelemetry/api - external_request_to_import_mapping("next/dist/compiled/@opentelemetry/api"), + external_if_node(pages_dir, "next/dist/compiled/@opentelemetry/api"), ); insert_alias_to_alternatives( import_map, format!("{VIRTUAL_PACKAGE_NAME}/pages/_app"), vec![ request_to_import_mapping(pages_dir, "./_app"), - external_request_to_import_mapping("next/app"), + external_if_node(pages_dir, "next/app"), ], ); insert_alias_to_alternatives( @@ -382,7 +390,7 @@ pub async fn insert_next_server_special_aliases( format!("{VIRTUAL_PACKAGE_NAME}/pages/_document"), vec![ request_to_import_mapping(pages_dir, "./_document"), - external_request_to_import_mapping("next/document"), + external_if_node(pages_dir, "next/document"), ], ); insert_alias_to_alternatives( @@ -390,7 +398,7 @@ pub async fn insert_next_server_special_aliases( format!("{VIRTUAL_PACKAGE_NAME}/pages/_error"), vec![ request_to_import_mapping(pages_dir, "./_error"), - external_request_to_import_mapping("next/error"), + external_if_node(pages_dir, "next/error"), ], ); } @@ -485,14 +493,15 @@ pub async fn insert_next_server_special_aliases( // * maps react-dom -> react-dom/server-rendering-stub // * passes through react and (react|react-dom|react-server-dom-webpack)/(.*) to // next/dist/compiled/react and next/dist/compiled/$1/$2 resp. - (NextMode::Build | NextMode::Development, ServerContextType::AppSSR { .. }) => { + (NextMode::Build | NextMode::Development, ServerContextType::AppSSR { app_dir }) => { import_map.insert_exact_alias( "react", - external_request_to_import_mapping("next/dist/compiled/react"), + external_if_node(app_dir, "next/dist/compiled/react"), ); import_map.insert_exact_alias( "react-dom", - external_request_to_import_mapping( + external_if_node( + app_dir, "next/dist/compiled/react-dom/server-rendering-stub", ), ); @@ -505,7 +514,7 @@ pub async fn insert_next_server_special_aliases( "next/dist/compiled/react-server-dom-webpack/*", ), ] { - let import_mapping = external_request_to_import_mapping(request); + let import_mapping = external_if_node(app_dir, request); import_map.insert_wildcard_alias(wildcard_alias, import_mapping); } } From 9e1d9579fe4052fca9c2b728cae57a43fd9bfd6e Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 3 Aug 2023 06:56:35 +0000 Subject: [PATCH 07/15] lint --- test/e2e/app-dir/app-simple-routes/app-simple-routes.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/e2e/app-dir/app-simple-routes/app-simple-routes.test.ts b/test/e2e/app-dir/app-simple-routes/app-simple-routes.test.ts index 9a08307c55d3e..87469a77fa2b8 100644 --- a/test/e2e/app-dir/app-simple-routes/app-simple-routes.test.ts +++ b/test/e2e/app-dir/app-simple-routes/app-simple-routes.test.ts @@ -1,6 +1,4 @@ import { createNextDescribe } from 'e2e-utils' -import { check } from 'next-test-utils' -import { Readable } from 'stream' const bathPath = process.env.BASE_PATH ?? '' @@ -9,7 +7,7 @@ createNextDescribe( { files: __dirname, }, - ({ next, isNextDeploy, isNextDev, isNextStart }) => { + ({ next }) => { describe('works with simple routes', () => { it('renders a node route', async () => { expect( From dd22f04cd1da969a1d9ec381b90f70dfaefc3378 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 3 Aug 2023 07:08:45 +0000 Subject: [PATCH 08/15] fix buildGetSafeRouteKey --- .../next-core/src/next_edge/route_regex.rs | 23 ++++++++++--------- .../shared/lib/router/utils/route-regex.ts | 15 ++++-------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs b/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs index e81f7480f7bff..5576f111c41f7 100644 --- a/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs +++ b/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs @@ -142,20 +142,21 @@ pub fn get_route_regex(normalized_route: &str) -> RouteRegex { /// Builds a function to generate a minimal routeKey using only a-z and minimal /// number of characters. fn build_get_safe_route_key() -> impl FnMut() -> String { - let mut route_key_char_code = 97; - let mut route_key_char_length = 1; - Box::new(move || { + let i = 0; + + move || { let mut route_key = String::new(); - for _ in 0..route_key_char_length { - route_key.push(route_key_char_code as u8 as char); - route_key_char_code += 1; - if route_key_char_code > 122 { - route_key_char_length += 1; - route_key_char_code = 97; - } + i += 1; + let mut j = i; + + while j > 0 { + route_key.push((97 + ((j - 1) % 26)) as char); + j = (j - 1) / 26; } + + i += 1; route_key - }) + } } fn get_safe_key_from_segment( diff --git a/packages/next/src/shared/lib/router/utils/route-regex.ts b/packages/next/src/shared/lib/router/utils/route-regex.ts index 848de809a7f92..177e01a20c274 100644 --- a/packages/next/src/shared/lib/router/utils/route-regex.ts +++ b/packages/next/src/shared/lib/router/utils/route-regex.ts @@ -83,19 +83,14 @@ export function getRouteRegex(normalizedRoute: string): RouteRegex { * number of characters. */ function buildGetSafeRouteKey() { - let routeKeyCharCode = 97 - let routeKeyCharLength = 1 + let i = 0 return () => { let routeKey = '' - for (let i = 0; i < routeKeyCharLength; i++) { - routeKey += String.fromCharCode(routeKeyCharCode) - routeKeyCharCode++ - - if (routeKeyCharCode > 122) { - routeKeyCharLength++ - routeKeyCharCode = 97 - } + let j = ++i + while (j > 0) { + routeKey += String.fromCharCode(97 + ((j - 1) % 26)) + j = Math.floor((j - 1) / 26) } return routeKey } From 52023a1f1246007fcd7bacc892274cafbc783f65 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 3 Aug 2023 07:35:06 +0000 Subject: [PATCH 09/15] remove debug statement --- packages/next/src/server/app-render/app-render.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/next/src/server/app-render/app-render.tsx b/packages/next/src/server/app-render/app-render.tsx index ad90a16546664..30439eebec15c 100644 --- a/packages/next/src/server/app-render/app-render.tsx +++ b/packages/next/src/server/app-render/app-render.tsx @@ -252,7 +252,6 @@ export async function renderToHTMLOrFlight( allCapturedErrors, }) - console.log(ComponentMod) patchFetch(ComponentMod) /** * Rules of Static & Dynamic HTML: From 4e7fc8cf17604b88695be20aa882c1dc3cf7e678 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 3 Aug 2023 08:05:03 +0000 Subject: [PATCH 10/15] fixup: fix buildGetSafeRouteKey --- .../next-swc/crates/next-core/src/next_edge/route_regex.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs b/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs index 5576f111c41f7..62f9b9d67fa82 100644 --- a/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs +++ b/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs @@ -142,7 +142,7 @@ pub fn get_route_regex(normalized_route: &str) -> RouteRegex { /// Builds a function to generate a minimal routeKey using only a-z and minimal /// number of characters. fn build_get_safe_route_key() -> impl FnMut() -> String { - let i = 0; + let mut i = 0; move || { let mut route_key = String::new(); @@ -150,7 +150,7 @@ fn build_get_safe_route_key() -> impl FnMut() -> String { let mut j = i; while j > 0 { - route_key.push((97 + ((j - 1) % 26)) as char); + route_key.push((97 + ((j - 1) % 26)) as u8 as char); j = (j - 1) / 26; } From d30dab25d06f1607c981a4664ef25ae626c2ca73 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Thu, 3 Aug 2023 09:08:12 +0000 Subject: [PATCH 11/15] clippy --- packages/next-swc/crates/next-api/src/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next-swc/crates/next-api/src/app.rs b/packages/next-swc/crates/next-api/src/app.rs index 933db84882375..fdb9a1e507b13 100644 --- a/packages/next-swc/crates/next-api/src/app.rs +++ b/packages/next-swc/crates/next-api/src/app.rs @@ -715,7 +715,7 @@ impl AppEndpoint { .await? .preferred_region .clone() - .map(|region| Regions::Single(region)), + .map(Regions::Single), matchers: vec![matchers], ..Default::default() }; From 8a802ea14ba57d86d571012083bd9ab161648323 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Mon, 7 Aug 2023 07:41:14 +0000 Subject: [PATCH 12/15] fix context in node.js app routes, add test case for react-server condition --- .../crates/next-core/src/app_source.rs | 62 +++++++++++++++++-- .../crates/next-core/src/next_edge/context.rs | 7 +-- .../next-core/src/next_server/context.rs | 7 +-- .../crates/next-core/src/next_server/mod.rs | 1 + .../src/next_server/route_transition.rs | 30 +++++++++ .../conditions/input/app/app-edge/page.js | 2 + .../conditions/input/app/app-nodejs/page.js | 2 + .../import/conditions/input/app/client.tsx | 2 + .../conditions/input/app/route-edge/route.js | 2 + .../input/app/route-nodejs/route.js | 2 + .../next/import/conditions/input/app/test.js | 12 ++++ .../import/conditions/input/middleware.js | 2 + .../input/node_modules/react-server | 1 + .../conditions/input/pages/api/api-edge.js | 2 + .../conditions/input/pages/api/api-nodejs.js | 2 + .../conditions/input/pages/page-edge.js | 2 + .../conditions/input/pages/page-nodejs.js | 2 + .../conditions/input/react-server/default.js | 1 + .../conditions/input/react-server/main.js | 1 + .../input/react-server/package.json | 9 +++ .../input/react-server/react-server.js | 1 + 21 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 packages/next-swc/crates/next-core/src/next_server/route_transition.rs create mode 120000 packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/node_modules/react-server create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/default.js create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/main.js create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/package.json create mode 100644 packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/react-server.js diff --git a/packages/next-swc/crates/next-core/src/app_source.rs b/packages/next-swc/crates/next-core/src/app_source.rs index 5f2723a6b6c79..b71824e72c08d 100644 --- a/packages/next-swc/crates/next-core/src/app_source.rs +++ b/packages/next-swc/crates/next-core/src/app_source.rs @@ -85,9 +85,12 @@ use crate::{ route_transition::NextEdgeRouteTransition, }, next_route_matcher::{NextFallbackMatcher, NextParamsMatcher}, - next_server::context::{ - get_server_compile_time_info, get_server_module_options_context, - get_server_resolve_options_context, ServerContextType, + next_server::{ + context::{ + get_server_compile_time_info, get_server_module_options_context, + get_server_resolve_options_context, ServerContextType, + }, + route_transition::NextRouteTransition, }, util::{render_data, NextRuntime}, }; @@ -231,12 +234,12 @@ fn next_server_component_transition( execution_context: Vc, app_dir: Vc, server_root: Vc, - mode: NextMode, process_env: Vc>, next_config: Vc, server_addr: Vc, ecmascript_client_reference_transition_name: Vc, ) -> Vc> { + let mode = NextMode::DevServer; let ty = Value::new(ServerContextType::AppRSC { app_dir, client_transition: None, @@ -261,6 +264,37 @@ fn next_server_component_transition( ) } +#[turbo_tasks::function] +fn next_route_transition( + project_path: Vc, + app_dir: Vc, + process_env: Vc>, + next_config: Vc, + server_addr: Vc, + execution_context: Vc, +) -> Vc> { + let mode = NextMode::DevServer; + let server_ty = Value::new(ServerContextType::AppRoute { app_dir }); + + let server_compile_time_info = get_server_compile_time_info(mode, process_env, server_addr); + + let server_resolve_options_context = get_server_resolve_options_context( + project_path, + server_ty, + mode, + next_config, + execution_context, + ); + + Vc::upcast( + NextRouteTransition { + server_compile_time_info, + server_resolve_options_context, + } + .cell(), + ) +} + #[turbo_tasks::function] fn next_edge_server_component_transition( project_path: Vc, @@ -422,6 +456,17 @@ fn app_context( execution_context, ), ); + transitions.insert( + "next-route".to_string(), + next_route_transition( + project_path, + app_dir, + env, + next_config, + server_addr, + execution_context, + ), + ); transitions.insert( "next-edge-page".to_string(), next_edge_page_transition( @@ -443,7 +488,6 @@ fn app_context( execution_context, app_dir, server_root, - mode, env, next_config, server_addr, @@ -1080,6 +1124,14 @@ impl AppRoute { Some(NextRuntime::NodeJs) | None => { let bootstrap_asset = next_asset("entry/app/route.ts".to_string()); + let entry_asset = this + .context + .with_transition("next-route".to_string()) + .process( + Vc::upcast(entry_file_source), + Value::new(ReferenceType::Entry(EntryReferenceSubType::AppRoute)), + ); + route_bootstrap( entry_asset, Vc::upcast(this.context), diff --git a/packages/next-swc/crates/next-core/src/next_edge/context.rs b/packages/next-swc/crates/next-core/src/next_edge/context.rs index 387d7795d1c69..54c7dd3331a63 100644 --- a/packages/next-swc/crates/next-core/src/next_edge/context.rs +++ b/packages/next-swc/crates/next-core/src/next_edge/context.rs @@ -96,14 +96,13 @@ pub async fn get_edge_resolve_options_context( ]; match ty { - ServerContextType::AppRSC { .. } - | ServerContextType::AppRoute { .. } - | ServerContextType::Middleware { .. } => { + ServerContextType::AppRSC { .. } | ServerContextType::AppRoute { .. } => { custom_conditions.push("react-server".to_string()) } ServerContextType::Pages { .. } | ServerContextType::PagesData { .. } - | ServerContextType::AppSSR { .. } => {} + | ServerContextType::AppSSR { .. } + | ServerContextType::Middleware { .. } => {} }; let resolve_options_context = ResolveOptionsContext { diff --git a/packages/next-swc/crates/next-core/src/next_server/context.rs b/packages/next-swc/crates/next-core/src/next_server/context.rs index a45ee357551c2..1f9b7e4aceb78 100644 --- a/packages/next-swc/crates/next-core/src/next_server/context.rs +++ b/packages/next-swc/crates/next-core/src/next_server/context.rs @@ -108,14 +108,13 @@ pub async fn get_server_resolve_options_context( let mut custom_conditions = vec![mode.node_env().to_string(), "node".to_string()]; match ty { - ServerContextType::AppRSC { .. } - | ServerContextType::AppRoute { .. } - | ServerContextType::Middleware { .. } => { + ServerContextType::AppRSC { .. } | ServerContextType::AppRoute { .. } => { custom_conditions.push("react-server".to_string()) } ServerContextType::Pages { .. } | ServerContextType::PagesData { .. } - | ServerContextType::AppSSR { .. } => {} + | ServerContextType::AppSSR { .. } + | ServerContextType::Middleware { .. } => {} }; let external_cjs_modules_plugin = ExternalCjsModulesResolvePlugin::new( project_path, diff --git a/packages/next-swc/crates/next-core/src/next_server/mod.rs b/packages/next-swc/crates/next-core/src/next_server/mod.rs index fbc8be4401df5..982e11165f5dc 100644 --- a/packages/next-swc/crates/next-core/src/next_server/mod.rs +++ b/packages/next-swc/crates/next-core/src/next_server/mod.rs @@ -1,5 +1,6 @@ pub(crate) mod context; pub(crate) mod resolve; +pub mod route_transition; pub(crate) mod transforms; pub use context::{ diff --git a/packages/next-swc/crates/next-core/src/next_server/route_transition.rs b/packages/next-swc/crates/next-core/src/next_server/route_transition.rs new file mode 100644 index 0000000000000..1c1de254b5b4a --- /dev/null +++ b/packages/next-swc/crates/next-core/src/next_server/route_transition.rs @@ -0,0 +1,30 @@ +use turbo_tasks::Vc; +use turbopack_binding::turbopack::{ + core::compile_time_info::CompileTimeInfo, + turbopack::{resolve_options_context::ResolveOptionsContext, transition::Transition}, +}; + +#[turbo_tasks::value(shared)] +pub struct NextRouteTransition { + pub server_compile_time_info: Vc, + pub server_resolve_options_context: Vc, +} + +#[turbo_tasks::value_impl] +impl Transition for NextRouteTransition { + #[turbo_tasks::function] + fn process_compile_time_info( + &self, + _compile_time_info: Vc, + ) -> Vc { + self.server_compile_time_info + } + + #[turbo_tasks::function] + fn process_resolve_options_context( + &self, + _context: Vc, + ) -> Vc { + self.server_resolve_options_context + } +} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/app-edge/page.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/app-edge/page.js index 5c729b53499d3..f431d9eb55fa3 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/app-edge/page.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/app-edge/page.js @@ -1,5 +1,6 @@ import edgeThenNode from 'edge-then-node' import nodeThenEdge from 'node-then-edge' +import reactServer from 'react-server' import ClientComponent from '../client' export const runtime = 'edge' @@ -11,6 +12,7 @@ export default function AppEdge() { {JSON.stringify({ edgeThenNode, nodeThenEdge, + reactServer, })} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/app-nodejs/page.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/app-nodejs/page.js index 7e95d6c6c64a6..4163f11a6d579 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/app-nodejs/page.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/app-nodejs/page.js @@ -1,5 +1,6 @@ import edgeThenNode from 'edge-then-node' import nodeThenEdge from 'node-then-edge' +import reactServer from 'react-server' import ClientComponent from '../client' export const runtime = 'nodejs' @@ -11,6 +12,7 @@ export default function AppNodeJs() { {JSON.stringify({ edgeThenNode, nodeThenEdge, + reactServer, })} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/client.tsx b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/client.tsx index 43fad65443249..bcf8e8330e78d 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/client.tsx +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/client.tsx @@ -2,6 +2,7 @@ import edgeThenNode from 'edge-then-node' import nodeThenEdge from 'node-then-edge' +import reactServer from 'react-server' export const runtime = 'edge' @@ -11,6 +12,7 @@ export default function ClientComponent() { {JSON.stringify({ edgeThenNode, nodeThenEdge, + reactServer, })} ) diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/route-edge/route.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/route-edge/route.js index 7f4a59c1e59c2..6fb0569daa1c8 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/route-edge/route.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/route-edge/route.js @@ -1,5 +1,6 @@ import edgeThenNode from 'edge-then-node' import nodeThenEdge from 'node-then-edge' +import reactServer from 'react-server' export const runtime = 'edge' @@ -7,5 +8,6 @@ export function GET() { return Response.json({ edgeThenNode, nodeThenEdge, + reactServer, }) } diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/route-nodejs/route.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/route-nodejs/route.js index 0acec74ac9b83..4973d98a24580 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/route-nodejs/route.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/route-nodejs/route.js @@ -1,5 +1,6 @@ import edgeThenNode from 'edge-then-node' import nodeThenEdge from 'node-then-edge' +import reactServer from 'react-server' export const runtime = 'nodejs' @@ -7,5 +8,6 @@ export function GET() { return Response.json({ edgeThenNode, nodeThenEdge, + reactServer, }) } diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/test.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/test.js index a0ad87ce72979..e2c2f40a06dd7 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/test.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/app/test.js @@ -39,6 +39,7 @@ function runTests() { server: { edgeThenNode: 'node', nodeThenEdge: 'node', + reactServer: 'default', }, }) }) @@ -51,6 +52,7 @@ function runTests() { server: { edgeThenNode: 'edge', nodeThenEdge: 'edge', + reactServer: 'default', }, }) // TODO: delete this. @@ -58,6 +60,7 @@ function runTests() { server: { edgeThenNode: 'node', nodeThenEdge: 'node', + reactServer: 'default', }, }) }) @@ -67,6 +70,7 @@ function runTests() { expect(json).toMatchObject({ edgeThenNode: 'node', nodeThenEdge: 'node', + reactServer: 'default', }) }) @@ -75,6 +79,7 @@ function runTests() { expect(json).toMatchObject({ edgeThenNode: 'edge', nodeThenEdge: 'edge', + reactServer: 'default', }) }) @@ -84,10 +89,12 @@ function runTests() { server: { edgeThenNode: 'node', nodeThenEdge: 'node', + reactServer: 'react-server', }, client: { edgeThenNode: 'node', nodeThenEdge: 'node', + reactServer: 'default', }, }) }) @@ -98,10 +105,12 @@ function runTests() { server: { edgeThenNode: 'edge', nodeThenEdge: 'edge', + reactServer: 'react-server', }, client: { edgeThenNode: 'edge', nodeThenEdge: 'edge', + reactServer: 'default', }, }) }) @@ -111,6 +120,7 @@ function runTests() { expect(json).toMatchObject({ edgeThenNode: 'node', nodeThenEdge: 'node', + reactServer: 'react-server', }) }) @@ -119,6 +129,7 @@ function runTests() { expect(json).toMatchObject({ edgeThenNode: 'edge', nodeThenEdge: 'edge', + reactServer: 'react-server', }) }) @@ -127,6 +138,7 @@ function runTests() { expect(json).toMatchObject({ edgeThenNode: 'edge', nodeThenEdge: 'edge', + reactServer: 'default', }) }) } diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/middleware.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/middleware.js index 3b850065d05bc..996404a62a6e1 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/middleware.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/middleware.js @@ -1,5 +1,6 @@ import edgeThenNode from 'edge-then-node' import nodeThenEdge from 'node-then-edge' +import reactServer from 'react-server' export const config = { matcher: '/middleware', @@ -9,5 +10,6 @@ export function middleware() { return Response.json({ edgeThenNode, nodeThenEdge, + reactServer, }) } diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/node_modules/react-server b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/node_modules/react-server new file mode 120000 index 0000000000000..a79f75d094cbe --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/node_modules/react-server @@ -0,0 +1 @@ +../react-server \ No newline at end of file diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/api/api-edge.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/api/api-edge.js index fda0edc342a15..d3282aebf2ec9 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/api/api-edge.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/api/api-edge.js @@ -1,5 +1,6 @@ import edgeThenNode from 'edge-then-node' import nodeThenEdge from 'node-then-edge' +import reactServer from 'react-server' export const config = { runtime: 'edge', @@ -10,5 +11,6 @@ export default function ApiEdge() { NEXT_RUNTIME: process.env.NEXT_RUNTIME, edgeThenNode, nodeThenEdge, + reactServer, }) } diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/api/api-nodejs.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/api/api-nodejs.js index d85d6db5cd962..3bba725f81eb9 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/api/api-nodejs.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/api/api-nodejs.js @@ -1,5 +1,6 @@ import edgeThenNode from 'edge-then-node' import nodeThenEdge from 'node-then-edge' +import reactServer from 'react-server' export const config = { runtime: 'nodejs', @@ -10,5 +11,6 @@ export default function ApiNodeJs(req, res) { NEXT_RUNTIME: process.env.NEXT_RUNTIME, edgeThenNode, nodeThenEdge, + reactServer, }) } diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/page-edge.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/page-edge.js index 5dbfc970d1e60..fd1961b0d7c72 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/page-edge.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/page-edge.js @@ -1,5 +1,6 @@ import edgeThenNode from 'edge-then-node' import nodeThenEdge from 'node-then-edge' +import reactServer from 'react-server' export const config = { runtime: 'experimental-edge', @@ -11,6 +12,7 @@ export default function PageEdge() { {JSON.stringify({ edgeThenNode, nodeThenEdge, + reactServer, })} ) diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/page-nodejs.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/page-nodejs.js index 3265030fadcea..0446641693fa5 100644 --- a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/page-nodejs.js +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/pages/page-nodejs.js @@ -1,5 +1,6 @@ import edgeThenNode from 'edge-then-node' import nodeThenEdge from 'node-then-edge' +import reactServer from 'react-server' export const config = { runtime: 'nodejs', @@ -11,6 +12,7 @@ export default function PageNodeJs() { {JSON.stringify({ edgeThenNode, nodeThenEdge, + reactServer, })} ) diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/default.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/default.js new file mode 100644 index 0000000000000..9837417e5254d --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/default.js @@ -0,0 +1 @@ +module.exports = 'default' diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/main.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/main.js new file mode 100644 index 0000000000000..699a0cf973d5a --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/main.js @@ -0,0 +1 @@ +module.exports = 'main' diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/package.json b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/package.json new file mode 100644 index 0000000000000..892355a072201 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/package.json @@ -0,0 +1,9 @@ +{ + "main": "main.js", + "exports": { + ".": { + "react-server": "./react-server.js", + "default": "./default.js" + } + } +} diff --git a/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/react-server.js b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/react-server.js new file mode 100644 index 0000000000000..7d4f4ffa049b2 --- /dev/null +++ b/packages/next-swc/crates/next-dev-tests/tests/integration/next/import/conditions/input/react-server/react-server.js @@ -0,0 +1 @@ +module.exports = 'react-server' From 6bd440b92e0706375ec3ae24d33e9652e308b1c5 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Mon, 7 Aug 2023 10:31:27 +0200 Subject: [PATCH 13/15] remove comment Co-authored-by: Alex Kirszenberg --- .../crates/next-core/src/next_edge/route_regex.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs b/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs index 62f9b9d67fa82..a28dba9fd7cac 100644 --- a/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs +++ b/packages/next-swc/crates/next-core/src/next_edge/route_regex.rs @@ -1,13 +1,5 @@ //! The following code was mostly generated using GTP-4 from //! next.js/packages/next/src/shared/lib/router/utils/route-regex.ts -//! -//! It contains errors and is not meant to be used as-is. -//! -//! The following should be changed: -//! * NamedMiddlewareRegex should just contain a string, not an actual Regex. -//! * There's plenty of places where more Rust-idiomatic string processing could -//! be used. -//! * Compilation errors. use std::collections::HashMap; From 1545e47e418a0c4943496b8dc44860f5f1a3d105 Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Mon, 7 Aug 2023 08:36:51 +0000 Subject: [PATCH 14/15] use enum instead of boolean --- .../crates/next-core/src/next_import_map.rs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/next-swc/crates/next-core/src/next_import_map.rs b/packages/next-swc/crates/next-core/src/next_import_map.rs index b19441600bfcd..48efca2a45a32 100644 --- a/packages/next-swc/crates/next-core/src/next_import_map.rs +++ b/packages/next-swc/crates/next-core/src/next_import_map.rs @@ -211,7 +211,8 @@ pub async fn get_next_server_import_map( let ty = ty.into_value(); - insert_next_server_special_aliases(&mut import_map, ty, mode, false).await?; + insert_next_server_special_aliases(&mut import_map, ty, mode, NextServerEnvironment::NodeJs) + .await?; let external = ImportMapping::External(None).cell(); match ty { @@ -283,7 +284,8 @@ pub async fn get_next_edge_import_map( let ty = ty.into_value(); - insert_next_server_special_aliases(&mut import_map, ty, mode, true).await?; + insert_next_server_special_aliases(&mut import_map, ty, mode, NextServerEnvironment::Edge) + .await?; match ty { ServerContextType::Pages { .. } | ServerContextType::PagesData { .. } => {} @@ -356,18 +358,21 @@ static NEXT_ALIASES: [(&str, &str); 23] = [ ("setImmediate", "next/dist/compiled/setimmediate"), ]; -pub async fn insert_next_server_special_aliases( +#[derive(Debug, Clone, Copy)] +enum NextServerEnvironment { + Edge, + NodeJs, +} + +async fn insert_next_server_special_aliases( import_map: &mut ImportMap, ty: ServerContextType, mode: NextMode, - is_edge: bool, + server_env: NextServerEnvironment, ) -> Result<()> { - let external_if_node = move |context_dir: Vc, request: &str| { - if is_edge { - request_to_import_mapping(context_dir, request) - } else { - external_request_to_import_mapping(request) - } + let external_if_node = move |context_dir: Vc, request: &str| match server_env { + NextServerEnvironment::Edge => request_to_import_mapping(context_dir, request), + NextServerEnvironment::NodeJs => external_request_to_import_mapping(request), }; match (mode, ty) { (_, ServerContextType::Pages { pages_dir }) => { From bfb458d6d29c4ab6416e1b60d5d3e97fca1485ad Mon Sep 17 00:00:00 2001 From: Tobias Koppers Date: Mon, 7 Aug 2023 08:38:27 +0000 Subject: [PATCH 15/15] use NextRuntime enum instead --- .../crates/next-core/src/next_import_map.rs | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/next-swc/crates/next-core/src/next_import_map.rs b/packages/next-swc/crates/next-core/src/next_import_map.rs index 48efca2a45a32..485e45c23126d 100644 --- a/packages/next-swc/crates/next-core/src/next_import_map.rs +++ b/packages/next-swc/crates/next-core/src/next_import_map.rs @@ -29,6 +29,7 @@ use crate::{ local::{NextFontLocalCssModuleReplacer, NextFontLocalReplacer}, }, next_server::context::ServerContextType, + util::NextRuntime, }; // Make sure to not add any external requests here. @@ -211,8 +212,7 @@ pub async fn get_next_server_import_map( let ty = ty.into_value(); - insert_next_server_special_aliases(&mut import_map, ty, mode, NextServerEnvironment::NodeJs) - .await?; + insert_next_server_special_aliases(&mut import_map, ty, mode, NextRuntime::NodeJs).await?; let external = ImportMapping::External(None).cell(); match ty { @@ -284,8 +284,7 @@ pub async fn get_next_edge_import_map( let ty = ty.into_value(); - insert_next_server_special_aliases(&mut import_map, ty, mode, NextServerEnvironment::Edge) - .await?; + insert_next_server_special_aliases(&mut import_map, ty, mode, NextRuntime::Edge).await?; match ty { ServerContextType::Pages { .. } | ServerContextType::PagesData { .. } => {} @@ -358,21 +357,15 @@ static NEXT_ALIASES: [(&str, &str); 23] = [ ("setImmediate", "next/dist/compiled/setimmediate"), ]; -#[derive(Debug, Clone, Copy)] -enum NextServerEnvironment { - Edge, - NodeJs, -} - async fn insert_next_server_special_aliases( import_map: &mut ImportMap, ty: ServerContextType, mode: NextMode, - server_env: NextServerEnvironment, + runtime: NextRuntime, ) -> Result<()> { - let external_if_node = move |context_dir: Vc, request: &str| match server_env { - NextServerEnvironment::Edge => request_to_import_mapping(context_dir, request), - NextServerEnvironment::NodeJs => external_request_to_import_mapping(request), + let external_if_node = move |context_dir: Vc, request: &str| match runtime { + NextRuntime::Edge => request_to_import_mapping(context_dir, request), + NextRuntime::NodeJs => external_request_to_import_mapping(request), }; match (mode, ty) { (_, ServerContextType::Pages { pages_dir }) => {