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 userland_path = userland_module.ident().path().await?;
44 let (file_type, function_name, page_path) = if is_proxy {
45 ("Proxy", "proxy", "/proxy")
46 } else {
47 ("Middleware", "middleware", "/middleware")
48 };
49
50 if let Some(ecma_module) =
52 ResolvedVc::try_sidecast::<Box<dyn EcmascriptChunkPlaceable>>(userland_module)
53 {
54 let exports = ecma_module.get_exports().await?;
55
56 let has_valid_export = match &*exports {
58 EcmascriptExports::EsmExports(esm_exports) => {
60 let esm_exports = esm_exports.await?;
61 let has_default = esm_exports.exports.contains_key("default");
62 let expected_named = function_name;
63 let has_named = esm_exports.exports.contains_key(expected_named);
64 has_default || has_named
65 }
66 EcmascriptExports::CommonJs | EcmascriptExports::Value => true,
68 EcmascriptExports::DynamicNamespace => true,
70 EcmascriptExports::None | EcmascriptExports::Unknown => true,
73 EcmascriptExports::EmptyCommonJs => false,
75 };
76
77 if !has_valid_export {
78 MiddlewareMissingExportIssue {
79 file_type: file_type.into(),
80 function_name: function_name.into(),
81 file_path: (*userland_path).clone(),
82 }
83 .resolved_cell()
84 .emit();
85
86 }
89 }
90 let mut incremental_cache_handler_import = None;
93 let mut cache_handler_inner_assets = fxindexmap! {};
94
95 for cache_handler_path in next_config
96 .cache_handler(project_root.clone())
97 .await?
98 .into_iter()
99 {
100 let cache_handler_inner = rcstr!("INNER_INCREMENTAL_CACHE_HANDLER");
101 incremental_cache_handler_import = Some(cache_handler_inner.clone());
102 let cache_handler_module = asset_context
103 .process(
104 Vc::upcast(FileSource::new(cache_handler_path.clone())),
105 ReferenceType::Undefined,
106 )
107 .module()
108 .to_resolved()
109 .await?;
110 cache_handler_inner_assets.insert(cache_handler_inner, cache_handler_module);
111 }
112
113 let source = load_next_js_template(
115 "middleware.js",
116 project_root,
117 [("VAR_USERLAND", INNER), ("VAR_DEFINITION_PAGE", page_path)],
118 [],
119 [(
120 "incrementalCacheHandler",
121 incremental_cache_handler_import.as_deref(),
122 )],
123 )
124 .await?;
125
126 let mut inner_assets = fxindexmap! {
127 rcstr!(INNER) => userland_module
128 };
129 inner_assets.extend(cache_handler_inner_assets);
130
131 let module = asset_context
132 .process(
133 source,
134 ReferenceType::Internal(ResolvedVc::cell(inner_assets)),
135 )
136 .module();
137
138 Ok(module)
139}
140
141#[turbo_tasks::value]
142struct MiddlewareMissingExportIssue {
143 file_type: RcStr, function_name: RcStr, file_path: FileSystemPath,
146}
147
148#[async_trait]
149#[turbo_tasks::value_impl]
150impl Issue for MiddlewareMissingExportIssue {
151 fn stage(&self) -> IssueStage {
152 IssueStage::Transform
153 }
154
155 fn severity(&self) -> IssueSeverity {
156 IssueSeverity::Error
157 }
158
159 async fn file_path(&self) -> Result<FileSystemPath> {
160 Ok(self.file_path.clone())
161 }
162
163 async fn title(&self) -> Result<StyledString> {
164 let title_text = format!(
165 "{} is missing expected function export name",
166 self.file_type
167 );
168 Ok(StyledString::Text(title_text.into()))
169 }
170
171 async fn description(&self) -> Result<Option<StyledString>> {
172 let type_description = if self.file_type == "Proxy" {
173 "proxy (previously called middleware)"
174 } else {
175 "middleware"
176 };
177
178 let migration_bullet = if self.file_type == "Proxy" {
179 "- You are migrating from `middleware` to `proxy`, but haven't updated the exported \
180 function.\n"
181 } else {
182 ""
183 };
184
185 let description_text = format!(
187 "This function is what Next.js runs for every request handled by this {}.\n\n\
188 Why this happens:\n\
189 {}\
190 - The file exists but doesn't export a function.\n\
191 - The export is not a function (e.g., an object or constant).\n\
192 - There's a syntax error preventing the export from being recognized.\n\n\
193 To fix it:\n\
194 - Ensure this file has either a default or \"{}\" function export.\n\n\
195 Learn more: https://nextjs.org/docs/messages/middleware-to-proxy",
196 type_description,
197 migration_bullet,
198 self.function_name
199 );
200
201 Ok(Some(StyledString::Text(description_text.into())))
202 }
203}