1use anyhow::Result;
2use async_trait::async_trait;
3use turbo_rcstr::{RcStr, rcstr};
4use turbo_tasks::{ResolvedVc, Vc, fxindexmap};
5use turbo_tasks_fs::FileSystemPath;
6use turbopack_core::{
7 context::AssetContext,
8 file_source::FileSource,
9 issue::{Issue, IssueExt, IssueSeverity, IssueStage, StyledString},
10 module::Module,
11 reference_type::ReferenceType,
12};
13use turbopack_ecmascript::chunk::{EcmascriptChunkPlaceable, EcmascriptExports};
14
15use crate::{next_config::NextConfig, util::load_next_js_template};
16
17#[turbo_tasks::function]
18pub async fn middleware_files(page_extensions: Vc<Vec<RcStr>>) -> Result<Vc<Vec<RcStr>>> {
19 let extensions = page_extensions.await?;
20 let files = ["middleware.", "src/middleware.", "proxy.", "src/proxy."]
21 .into_iter()
22 .flat_map(|f| {
23 extensions
24 .iter()
25 .map(move |ext| String::from(f) + ext.as_str())
26 .map(RcStr::from)
27 })
28 .collect();
29 Ok(Vc::cell(files))
30}
31
32#[turbo_tasks::function]
33pub async fn get_middleware_module(
34 asset_context: Vc<Box<dyn AssetContext>>,
35 project_root: FileSystemPath,
36 userland_module: ResolvedVc<Box<dyn Module>>,
37 is_proxy: bool,
38 next_config: Vc<NextConfig>,
39) -> Result<Vc<Box<dyn Module>>> {
40 const INNER: &str = "INNER_MIDDLEWARE_MODULE";
41
42 let (file_type, function_name, page_path) = if is_proxy {
43 ("Proxy", "proxy", "/proxy")
44 } else {
45 ("Middleware", "middleware", "/middleware")
46 };
47
48 if let Some(ecma_module) =
50 ResolvedVc::try_sidecast::<Box<dyn EcmascriptChunkPlaceable>>(userland_module)
51 {
52 let exports = ecma_module.get_exports().await?;
53
54 let has_valid_export = match &*exports {
56 EcmascriptExports::EsmExports(esm_exports) => {
58 let esm_exports = esm_exports.await?;
59 let has_default = esm_exports.exports.contains_key("default");
60 let expected_named = function_name;
61 let has_named = esm_exports.exports.contains_key(expected_named);
62 has_default || has_named
63 }
64 EcmascriptExports::CommonJs | EcmascriptExports::Value => true,
66 EcmascriptExports::DynamicNamespace => true,
68 EcmascriptExports::None | EcmascriptExports::Unknown => true,
71 EcmascriptExports::EmptyCommonJs => false,
73 };
74
75 if !has_valid_export {
76 MiddlewareMissingExportIssue {
77 file_type: file_type.into(),
78 function_name: function_name.into(),
79 file_path: userland_module.ident().await?.path.clone(),
80 }
81 .resolved_cell()
82 .emit();
83
84 }
87 }
88 let mut incremental_cache_handler_import = None;
91 let mut cache_handler_inner_assets = fxindexmap! {};
92
93 for cache_handler_path in next_config
94 .cache_handler(project_root.clone())
95 .await?
96 .into_iter()
97 {
98 let cache_handler_inner = rcstr!("INNER_INCREMENTAL_CACHE_HANDLER");
99 incremental_cache_handler_import = Some(cache_handler_inner.clone());
100 let cache_handler_module = asset_context
101 .process(
102 Vc::upcast(FileSource::new(cache_handler_path.clone())),
103 ReferenceType::Undefined,
104 )
105 .module()
106 .to_resolved()
107 .await?;
108 cache_handler_inner_assets.insert(cache_handler_inner, cache_handler_module);
109 }
110
111 let source = load_next_js_template(
113 "middleware.js",
114 project_root,
115 [("VAR_USERLAND", INNER), ("VAR_DEFINITION_PAGE", page_path)],
116 [],
117 [(
118 "incrementalCacheHandler",
119 incremental_cache_handler_import.as_deref(),
120 )],
121 )
122 .await?;
123
124 let mut inner_assets = fxindexmap! {
125 rcstr!(INNER) => userland_module
126 };
127 inner_assets.extend(cache_handler_inner_assets);
128
129 let module = asset_context
130 .process(
131 source,
132 ReferenceType::Internal(ResolvedVc::cell(inner_assets)),
133 )
134 .module();
135
136 Ok(module)
137}
138
139#[turbo_tasks::value]
140struct MiddlewareMissingExportIssue {
141 file_type: RcStr, function_name: RcStr, file_path: FileSystemPath,
144}
145
146#[async_trait]
147#[turbo_tasks::value_impl]
148impl Issue for MiddlewareMissingExportIssue {
149 fn stage(&self) -> IssueStage {
150 IssueStage::Transform
151 }
152
153 fn severity(&self) -> IssueSeverity {
154 IssueSeverity::Error
155 }
156
157 async fn file_path(&self) -> Result<FileSystemPath> {
158 Ok(self.file_path.clone())
159 }
160
161 async fn title(&self) -> Result<StyledString> {
162 let title_text = format!(
163 "{} is missing expected function export name",
164 self.file_type
165 );
166 Ok(StyledString::Text(title_text.into()))
167 }
168
169 async fn description(&self) -> Result<Option<StyledString>> {
170 let type_description = if self.file_type == "Proxy" {
171 "proxy (previously called middleware)"
172 } else {
173 "middleware"
174 };
175
176 let migration_bullet = if self.file_type == "Proxy" {
177 "- You are migrating from `middleware` to `proxy`, but haven't updated the exported \
178 function.\n"
179 } else {
180 ""
181 };
182
183 let description_text = format!(
185 "This function is what Next.js runs for every request handled by this {}.\n\n\
186 Why this happens:\n\
187 {}\
188 - The file exists but doesn't export a function.\n\
189 - The export is not a function (e.g., an object or constant).\n\
190 - There's a syntax error preventing the export from being recognized.\n\n\
191 To fix it:\n\
192 - Ensure this file has either a default or \"{}\" function export.\n\n\
193 Learn more: https://nextjs.org/docs/messages/middleware-to-proxy",
194 type_description, migration_bullet, self.function_name
195 );
196
197 Ok(Some(StyledString::Text(description_text.into())))
198 }
199}