next_core/
middleware.rs

1use anyhow::Result;
2use turbo_rcstr::{RcStr, rcstr};
3use turbo_tasks::{ResolvedVc, Vc, fxindexmap};
4use turbo_tasks_fs::FileSystemPath;
5use turbopack_core::{
6    context::AssetContext,
7    issue::{Issue, IssueExt, IssueSeverity, IssueStage, OptionStyledString, StyledString},
8    module::Module,
9    reference_type::ReferenceType,
10};
11use turbopack_ecmascript::chunk::{EcmascriptChunkPlaceable, EcmascriptExports};
12
13use crate::util::load_next_js_template;
14
15#[turbo_tasks::function]
16pub async fn middleware_files(page_extensions: Vc<Vec<RcStr>>) -> Result<Vc<Vec<RcStr>>> {
17    let extensions = page_extensions.await?;
18    let files = ["middleware.", "src/middleware.", "proxy.", "src/proxy."]
19        .into_iter()
20        .flat_map(|f| {
21            extensions
22                .iter()
23                .map(move |ext| String::from(f) + ext.as_str())
24                .map(RcStr::from)
25        })
26        .collect();
27    Ok(Vc::cell(files))
28}
29
30#[turbo_tasks::function]
31pub async fn get_middleware_module(
32    asset_context: Vc<Box<dyn AssetContext>>,
33    project_root: FileSystemPath,
34    userland_module: ResolvedVc<Box<dyn Module>>,
35    is_proxy: bool,
36) -> Result<Vc<Box<dyn Module>>> {
37    const INNER: &str = "INNER_MIDDLEWARE_MODULE";
38
39    // Determine if this is a proxy file by checking the module path
40    let userland_path = userland_module.ident().path().await?;
41    let (file_type, function_name, page_path) = if is_proxy {
42        ("Proxy", "proxy", "/proxy")
43    } else {
44        ("Middleware", "middleware", "/middleware")
45    };
46
47    // Validate that the module has the required exports
48    if let Some(ecma_module) =
49        Vc::try_resolve_sidecast::<Box<dyn EcmascriptChunkPlaceable>>(*userland_module).await?
50    {
51        let exports = ecma_module.get_exports().await?;
52
53        // Check if the module has the required exports
54        let has_valid_export = match &*exports {
55            // ESM modules - check for named or default export
56            EcmascriptExports::EsmExports(esm_exports) => {
57                let esm_exports = esm_exports.await?;
58                let has_default = esm_exports.exports.contains_key("default");
59                let expected_named = function_name;
60                let has_named = esm_exports.exports.contains_key(expected_named);
61                has_default || has_named
62            }
63            // CommonJS modules are valid (they can have module.exports or exports.default)
64            EcmascriptExports::CommonJs | EcmascriptExports::Value => true,
65            // DynamicNamespace might be valid for certain module types
66            EcmascriptExports::DynamicNamespace => true,
67            // None/Unknown likely indicate parsing errors - skip validation
68            // The parsing error will be emitted separately by Turbopack
69            EcmascriptExports::None | EcmascriptExports::Unknown => true,
70            // EmptyCommonJs is a legitimate case of missing exports
71            EcmascriptExports::EmptyCommonJs => false,
72        };
73
74        if !has_valid_export {
75            MiddlewareMissingExportIssue {
76                file_type: file_type.into(),
77                function_name: function_name.into(),
78                file_path: (*userland_path).clone(),
79            }
80            .resolved_cell()
81            .emit();
82
83            // Continue execution instead of bailing - let the module be processed anyway
84            // The runtime template will still catch this at runtime
85        }
86    }
87    // If we can't cast to EcmascriptChunkPlaceable, continue without validation
88    // (might be a special module type that doesn't support export checking)
89
90    // Load the file from the next.js codebase.
91    let source = load_next_js_template(
92        "middleware.js",
93        project_root,
94        &[("VAR_USERLAND", INNER), ("VAR_DEFINITION_PAGE", page_path)],
95        &[],
96        &[],
97    )
98    .await?;
99
100    let inner_assets = fxindexmap! {
101        rcstr!(INNER) => userland_module
102    };
103
104    let module = asset_context
105        .process(
106            source,
107            ReferenceType::Internal(ResolvedVc::cell(inner_assets)),
108        )
109        .module();
110
111    Ok(module)
112}
113
114#[turbo_tasks::value]
115struct MiddlewareMissingExportIssue {
116    file_type: RcStr,     // "Proxy" or "Middleware"
117    function_name: RcStr, // "proxy" or "middleware"
118    file_path: FileSystemPath,
119}
120
121#[turbo_tasks::value_impl]
122impl Issue for MiddlewareMissingExportIssue {
123    #[turbo_tasks::function]
124    fn stage(&self) -> Vc<IssueStage> {
125        IssueStage::Transform.into()
126    }
127
128    fn severity(&self) -> IssueSeverity {
129        IssueSeverity::Error
130    }
131
132    #[turbo_tasks::function]
133    fn file_path(&self) -> Vc<FileSystemPath> {
134        self.file_path.clone().cell()
135    }
136
137    #[turbo_tasks::function]
138    async fn title(&self) -> Result<Vc<StyledString>> {
139        let title_text = format!(
140            "{} is missing expected function export name",
141            self.file_type
142        );
143
144        Ok(StyledString::Text(title_text.into()).cell())
145    }
146
147    #[turbo_tasks::function]
148    async fn description(&self) -> Result<Vc<OptionStyledString>> {
149        let type_description = if self.file_type == "Proxy" {
150            "proxy (previously called middleware)"
151        } else {
152            "middleware"
153        };
154
155        let migration_bullet = if self.file_type == "Proxy" {
156            "- You are migrating from `middleware` to `proxy`, but haven't updated the exported \
157             function.\n"
158        } else {
159            ""
160        };
161
162        // Rest of the message goes in description to avoid formatIssue indentation
163        let description_text = format!(
164            "This function is what Next.js runs for every request handled by this {}.\n\n\
165             Why this happens:\n\
166             {}\
167             - The file exists but doesn't export a function.\n\
168             - The export is not a function (e.g., an object or constant).\n\
169             - There's a syntax error preventing the export from being recognized.\n\n\
170             To fix it:\n\
171             - Ensure this file has either a default or \"{}\" function export.\n\n\
172             Learn more: https://nextjs.org/docs/messages/middleware-to-proxy",
173            type_description,
174            migration_bullet,
175            self.function_name
176        );
177
178        Ok(Vc::cell(Some(
179            StyledString::Text(description_text.into()).resolved_cell(),
180        )))
181    }
182}