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