From befc6fa1f3b59971f146117032da95d5c41bdb11 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Mon, 1 Dec 2025 17:49:02 +0100 Subject: [PATCH 1/5] [turbopack] move edge entry wrapper to build template --- crates/next-core/src/next_edge/entry.rs | 52 +++++++------------ .../next/src/build/templates/edge-wrapper.ts | 15 ++++++ 2 files changed, 33 insertions(+), 34 deletions(-) create mode 100644 packages/next/src/build/templates/edge-wrapper.ts diff --git a/crates/next-core/src/next_edge/entry.rs b/crates/next-core/src/next_edge/entry.rs index bd51ee66032e0..45da574830827 100644 --- a/crates/next-core/src/next_edge/entry.rs +++ b/crates/next-core/src/next_edge/entry.rs @@ -1,16 +1,13 @@ use anyhow::Result; -use indoc::formatdoc; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{ResolvedVc, Vc, fxindexmap}; -use turbo_tasks_fs::{File, FileContent, FileSystemPath}; -use turbopack_core::{ - asset::AssetContent, context::AssetContext, module::Module, reference_type::ReferenceType, - virtual_source::VirtualSource, -}; -use turbopack_ecmascript::utils::StringifyJs; +use turbo_tasks_fs::FileSystemPath; +use turbopack_core::{context::AssetContext, module::Module, reference_type::ReferenceType}; + +use crate::util::load_next_js_template; #[turbo_tasks::function] -pub fn wrap_edge_entry( +pub async fn wrap_edge_entry( asset_context: Vc>, project_root: FileSystemPath, entry: ResolvedVc>, @@ -24,31 +21,18 @@ pub fn wrap_edge_entry( // (e.g. edge functions/middleware, this is what the Edge Runtime does). // Catch promise to prevent UnhandledPromiseRejectionWarning, this will be propagated through // the awaited export(s) anyway. - let source = formatdoc!( - r#" - self._ENTRIES ||= {{}}; - const modProm = import('MODULE'); - modProm.catch(() => {{}}); - self._ENTRIES[{}] = new Proxy(modProm, {{ - get(modProm, name) {{ - if (name === "then") {{ - return (res, rej) => modProm.then(res, rej); - }} - let result = (...args) => modProm.then((mod) => (0, mod[name])(...args)); - result.then = (res, rej) => modProm.then((mod) => mod[name]).then(res, rej); - return result; - }}, - }}); - "#, - StringifyJs(&format_args!("middleware_{pathname}")) - ); - let file = File::from(source); - - // TODO(alexkirsz) Figure out how to name this virtual asset. - let virtual_source = VirtualSource::new( - project_root.join("edge-wrapper.js")?, - AssetContent::file(FileContent::Content(file).cell()), - ); + // + // The actual wrapper lives in the Next.js templates directory as `edge-wrapper.js`. + // We use the template expansion helper so this code is kept in sync with other + // Next.js runtime templates. + let template_source = load_next_js_template( + "edge-wrapper.js", + project_root, + &[("VAR_ENTRY_NAME", &format!("middleware_{pathname}"))], + &[], + &[], + ) + .await?; let inner_assets = fxindexmap! { rcstr!("MODULE") => entry @@ -56,7 +40,7 @@ pub fn wrap_edge_entry( Ok(asset_context .process( - Vc::upcast(virtual_source), + template_source, ReferenceType::Internal(ResolvedVc::cell(inner_assets)), ) .module()) diff --git a/packages/next/src/build/templates/edge-wrapper.ts b/packages/next/src/build/templates/edge-wrapper.ts new file mode 100644 index 0000000000000..fe9748cd1e9cf --- /dev/null +++ b/packages/next/src/build/templates/edge-wrapper.ts @@ -0,0 +1,15 @@ +;(self as any)._ENTRIES ||= {} +const modProm: Promise = import('MODULE' as any) +modProm.catch(() => {}) +;(self as any)._ENTRIES['VAR_ENTRY_NAME'] = new Proxy(modProm, { + get(innerModProm: Promise, name: any) { + if (name === 'then') { + return (res: any, rej: any) => innerModProm.then(res, rej) + } + let result: any = (...args: any[]) => + innerModProm.then((mod: any) => (0, (mod as any)[name])(...args)) + result.then = (res: any, rej: any) => + innerModProm.then((mod: any) => (mod as any)[name]).then(res, rej) + return result + }, +}) From 8b9e20d4884006a60f52d480ad1bf8e0f0351d15 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Mon, 1 Dec 2025 18:25:13 +0100 Subject: [PATCH 2/5] api for no imports --- crates/next-core/src/next_edge/entry.rs | 8 ++-- crates/next-core/src/util.rs | 37 +++++++++++++++++- crates/next-taskless/src/lib.rs | 51 +++++++++++++++++++++++-- 3 files changed, 88 insertions(+), 8 deletions(-) diff --git a/crates/next-core/src/next_edge/entry.rs b/crates/next-core/src/next_edge/entry.rs index 45da574830827..37802c5c656a6 100644 --- a/crates/next-core/src/next_edge/entry.rs +++ b/crates/next-core/src/next_edge/entry.rs @@ -4,7 +4,7 @@ use turbo_tasks::{ResolvedVc, Vc, fxindexmap}; use turbo_tasks_fs::FileSystemPath; use turbopack_core::{context::AssetContext, module::Module, reference_type::ReferenceType}; -use crate::util::load_next_js_template; +use crate::util::load_next_js_template_no_imports; #[turbo_tasks::function] pub async fn wrap_edge_entry( @@ -24,8 +24,10 @@ pub async fn wrap_edge_entry( // // The actual wrapper lives in the Next.js templates directory as `edge-wrapper.js`. // We use the template expansion helper so this code is kept in sync with other - // Next.js runtime templates. - let template_source = load_next_js_template( + // Next.js runtime templates. This particular template does not have any imports + // of its own, so we use the variant that allows templates without relative + // imports to be rewritten. + let template_source = load_next_js_template_no_imports( "edge-wrapper.js", project_root, &[("VAR_ENTRY_NAME", &format!("middleware_{pathname}"))], diff --git a/crates/next-core/src/util.rs b/crates/next-core/src/util.rs index c46ec8bda89fd..126930c931acb 100644 --- a/crates/next-core/src/util.rs +++ b/crates/next-core/src/util.rs @@ -1,7 +1,7 @@ use std::{fmt::Display, str::FromStr}; use anyhow::{Result, anyhow, bail}; -use next_taskless::expand_next_js_template; +use next_taskless::{expand_next_js_template, expand_next_js_template_no_imports}; use serde::{Deserialize, Serialize, de::DeserializeOwned}; use turbo_rcstr::{RcStr, rcstr}; use turbo_tasks::{FxIndexMap, NonLocalValue, TaskInput, Vc, trace::TraceRawVcs}; @@ -268,6 +268,41 @@ pub async fn load_next_js_template( Ok(Vc::upcast(source)) } +/// Loads a next.js template but does **not** require that any relative imports are present +/// or rewritten. This is intended for small internal templates that do not have their own +/// imports but still use template variables/injections. +pub async fn load_next_js_template_no_imports( + template_path: &str, + project_path: FileSystemPath, + replacements: &[(&str, &str)], + injections: &[(&str, &str)], + imports: &[(&str, Option<&str>)], +) -> Result>> { + let template_path = virtual_next_js_template_path(project_path.clone(), template_path).await?; + + let content = file_content_rope(template_path.read()).await?; + let content = content.to_str()?; + + let package_root = get_next_package(project_path).await?; + + let content = expand_next_js_template_no_imports( + &content, + &template_path.path, + &package_root.path, + replacements.iter().copied(), + injections.iter().copied(), + imports.iter().copied(), + )?; + + let file = File::from(content); + let source = VirtualSource::new( + template_path, + AssetContent::file(FileContent::Content(file).cell()), + ); + + Ok(Vc::upcast(source)) +} + #[turbo_tasks::function] pub async fn file_content_rope(content: Vc) -> Result> { let content = &*content.await?; diff --git a/crates/next-taskless/src/lib.rs b/crates/next-taskless/src/lib.rs index 25ed1da4c553f..58f7f1745912a 100644 --- a/crates/next-taskless/src/lib.rs +++ b/crates/next-taskless/src/lib.rs @@ -25,6 +25,48 @@ pub fn expand_next_js_template<'a>( replacements: impl IntoIterator, injections: impl IntoIterator, imports: impl IntoIterator)>, +) -> Result { + expand_next_js_template_inner( + content, + template_path, + next_package_dir_path, + replacements, + injections, + imports, + true, + ) +} + +/// Same as [`expand_next_js_template`], but does not enforce that at least one relative +/// import is present and rewritten. This is useful for very small templates that only +/// use template variables/injections and have no imports of their own. +pub fn expand_next_js_template_no_imports<'a>( + content: &str, + template_path: &str, + next_package_dir_path: &str, + replacements: impl IntoIterator, + injections: impl IntoIterator, + imports: impl IntoIterator)>, +) -> Result { + expand_next_js_template_inner( + content, + template_path, + next_package_dir_path, + replacements, + injections, + imports, + false, + ) +} + +fn expand_next_js_template_inner<'a>( + content: &str, + template_path: &str, + next_package_dir_path: &str, + replacements: impl IntoIterator, + injections: impl IntoIterator, + imports: impl IntoIterator)>, + require_import_replacement: bool, ) -> Result { let template_parent_path = normalize_path(get_parent_path(template_path)) .context("failed to normalize template path")?; @@ -92,10 +134,11 @@ pub fn expand_next_js_template<'a>( }) .context("replacing imports failed")?; - // Verify that at least one import was replaced. It's the case today where every template file - // has at least one import to update, so this ensures that we don't accidentally remove the - // import replacement code or use the wrong template file. - if count == 0 { + // Verify that at least one import was replaced when required. It's the case today where every + // template file (except a few small internal helpers) has at least one import to update, so + // this ensures that we don't accidentally remove the import replacement code or use the wrong + // template file. + if require_import_replacement && count == 0 { bail!("Invariant: Expected to replace at least one import") } From 84ad8b66de37d5482be96cd96cce23f486c60564 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 2 Dec 2025 22:35:43 +0100 Subject: [PATCH 3/5] use jsmodule --- packages/next/src/build/templates/edge-wrapper.js | 15 +++++++++++++++ packages/next/src/build/templates/edge-wrapper.ts | 15 --------------- 2 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 packages/next/src/build/templates/edge-wrapper.js delete mode 100644 packages/next/src/build/templates/edge-wrapper.ts diff --git a/packages/next/src/build/templates/edge-wrapper.js b/packages/next/src/build/templates/edge-wrapper.js new file mode 100644 index 0000000000000..e148fddc69dc9 --- /dev/null +++ b/packages/next/src/build/templates/edge-wrapper.js @@ -0,0 +1,15 @@ +self._ENTRIES ||= {} +const modProm = import('MODULE') +modProm.catch(() => {}) +self._ENTRIES['VAR_ENTRY_NAME'] = new Proxy(modProm, { + get(innerModProm, name) { + if (name === 'then') { + return (res, rej) => innerModProm.then(res, rej) + } + let result = (...args) => + innerModProm.then((mod) => (0, mod[name])(...args)) + result.then = (res, rej) => + innerModProm.then((mod) => mod[name]).then(res, rej) + return result + }, +}) diff --git a/packages/next/src/build/templates/edge-wrapper.ts b/packages/next/src/build/templates/edge-wrapper.ts deleted file mode 100644 index fe9748cd1e9cf..0000000000000 --- a/packages/next/src/build/templates/edge-wrapper.ts +++ /dev/null @@ -1,15 +0,0 @@ -;(self as any)._ENTRIES ||= {} -const modProm: Promise = import('MODULE' as any) -modProm.catch(() => {}) -;(self as any)._ENTRIES['VAR_ENTRY_NAME'] = new Proxy(modProm, { - get(innerModProm: Promise, name: any) { - if (name === 'then') { - return (res: any, rej: any) => innerModProm.then(res, rej) - } - let result: any = (...args: any[]) => - innerModProm.then((mod: any) => (0, (mod as any)[name])(...args)) - result.then = (res: any, rej: any) => - innerModProm.then((mod: any) => (mod as any)[name]).then(res, rej) - return result - }, -}) From 4b4c2ee1be1829101df7f126be2a21e1837ab65d Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 2 Dec 2025 23:55:17 +0100 Subject: [PATCH 4/5] Add comments to edge-wrapper.js for clarity --- packages/next/src/build/templates/edge-wrapper.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/next/src/build/templates/edge-wrapper.js b/packages/next/src/build/templates/edge-wrapper.js index e148fddc69dc9..3b2987af8f07e 100644 --- a/packages/next/src/build/templates/edge-wrapper.js +++ b/packages/next/src/build/templates/edge-wrapper.js @@ -1,3 +1,11 @@ +// The wrapped module could be an async module, we handle that with the proxy +// here. The comma expression makes sure we don't call the function with the +// module as the "this" arg. +// Turn exports into functions that are also a thenable. This way you can await the whole object +// or exports (e.g. for Components) or call them directly as though they are async functions +// (e.g. edge functions/middleware, this is what the Edge Runtime does). +// Catch promise to prevent UnhandledPromiseRejectionWarning, this will be propagated through +// the awaited export(s) anyway. self._ENTRIES ||= {} const modProm = import('MODULE') modProm.catch(() => {}) From 06be261b6445067cde016748b907a441bbb0d0ae Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Tue, 2 Dec 2025 23:56:09 +0100 Subject: [PATCH 5/5] Clean up comments in entry.rs Removed comments explaining async module handling. --- crates/next-core/src/next_edge/entry.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/crates/next-core/src/next_edge/entry.rs b/crates/next-core/src/next_edge/entry.rs index 37802c5c656a6..e7d8b17703c73 100644 --- a/crates/next-core/src/next_edge/entry.rs +++ b/crates/next-core/src/next_edge/entry.rs @@ -13,15 +13,6 @@ pub async fn wrap_edge_entry( entry: ResolvedVc>, pathname: RcStr, ) -> Result>> { - // The wrapped module could be an async module, we handle that with the proxy - // here. The comma expression makes sure we don't call the function with the - // module as the "this" arg. - // Turn exports into functions that are also a thenable. This way you can await the whole object - // or exports (e.g. for Components) or call them directly as though they are async functions - // (e.g. edge functions/middleware, this is what the Edge Runtime does). - // Catch promise to prevent UnhandledPromiseRejectionWarning, this will be propagated through - // the awaited export(s) anyway. - // // The actual wrapper lives in the Next.js templates directory as `edge-wrapper.js`. // We use the template expansion helper so this code is kept in sync with other // Next.js runtime templates. This particular template does not have any imports