Skip to main content

next_core/
middleware.rs

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    // Validate that the module has the required exports
49    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        // Check if the module has the required exports
55        let has_valid_export = match &*exports {
56            // ESM modules - check for named or default export
57            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            // CommonJS modules are valid (they can have module.exports or exports.default)
65            EcmascriptExports::CommonJs | EcmascriptExports::Value => true,
66            // DynamicNamespace might be valid for certain module types
67            EcmascriptExports::DynamicNamespace => true,
68            // None/Unknown likely indicate parsing errors - skip validation
69            // The parsing error will be emitted separately by Turbopack
70            EcmascriptExports::None | EcmascriptExports::Unknown => true,
71            // EmptyCommonJs is a legitimate case of missing exports
72            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            // Continue execution instead of bailing - let the module be processed anyway
85            // The runtime template will still catch this at runtime
86        }
87    }
88    // If we can't cast to EcmascriptChunkPlaceable, continue without validation
89    // (might be a special module type that doesn't support export checking)
90    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    // Load the file from the next.js codebase.
112    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,     // "Proxy" or "Middleware"
142    function_name: RcStr, // "proxy" or "middleware"
143    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        // Rest of the message goes in description to avoid formatIssue indentation
184        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}