diff --git a/Cargo.lock b/Cargo.lock index cca56b629f964..efa500f3d8c07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4322,6 +4322,7 @@ dependencies = [ "next-custom-transforms", "next-taskless", "once_cell", + "pathdiff", "percent-encoding", "qstring", "react_remove_properties", diff --git a/crates/next-core/src/middleware.rs b/crates/next-core/src/middleware.rs index 49c4a44c73eca..56845fd72accf 100644 --- a/crates/next-core/src/middleware.rs +++ b/crates/next-core/src/middleware.rs @@ -139,23 +139,48 @@ impl Issue for MiddlewareMissingExportIssue { } #[turbo_tasks::function] - fn title(&self) -> Vc { - let file_name = self.file_path.file_name(); - - StyledString::Line(vec![ - StyledString::Text(rcstr!("The ")), - StyledString::Code(self.file_type.clone()), - StyledString::Text(rcstr!(" file \"")), - StyledString::Code(format!("./{}", file_name).into()), - StyledString::Text(rcstr!("\" must export a function named ")), - StyledString::Code(format!("`{}`", self.function_name).into()), - StyledString::Text(rcstr!(" or a default function.")), - ]) - .cell() + async fn title(&self) -> Result> { + let title_text = format!( + "{} is missing expected function export name", + self.file_type + ); + + Ok(StyledString::Text(title_text.into()).cell()) } #[turbo_tasks::function] - fn description(&self) -> Vc { - Vc::cell(None) + async fn description(&self) -> Result> { + let type_description = if self.file_type == "Proxy" { + "proxy (previously called middleware)" + } else { + "middleware" + }; + + let migration_bullet = if self.file_type == "Proxy" { + "- You are migrating from `middleware` to `proxy`, but haven't updated the exported \ + function.\n" + } else { + "" + }; + + // Rest of the message goes in description to avoid formatIssue indentation + let description_text = format!( + "This function is what Next.js runs for every request handled by this {}.\n\n\ + Why this happens:\n\ + {}\ + - The file exists but doesn't export a function.\n\ + - The export is not a function (e.g., an object or constant).\n\ + - There's a syntax error preventing the export from being recognized.\n\n\ + To fix it:\n\ + - Ensure this file has either a default or \"{}\" function export.\n\n\ + Learn more: https://nextjs.org/docs/messages/middleware-to-proxy", + type_description, + migration_bullet, + self.function_name + ); + + Ok(Vc::cell(Some( + StyledString::Text(description_text.into()).resolved_cell(), + ))) } } diff --git a/packages/next/errors.json b/packages/next/errors.json index 7563516b715a3..cbe2d5256c427 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -900,5 +900,6 @@ "899": "Both \"%s\" and \"%s\" files are detected. Please use \"%s\" instead. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy", "900": "Both %s file \"./%s\" and %s file \"./%s\" are detected. Please use \"./%s\" only. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy", "901": "Invalid \"cacheHandlers\" provided, expected an object e.g. { default: '/my-handler.js' }, received %s", - "902": "Invalid handler fields configured for \"cacheHandlers\":\\n%s" + "902": "Invalid handler fields configured for \"cacheHandlers\":\\n%s", + "903": "The file \"%s\" must export a function, either as a default export or as a named \"%s\" export.\\nThis function is what Next.js runs for every request handled by this %s.\\n\\nWhy this happens:\\n%s- The file exists but doesn't export a function.\\n- The export is not a function (e.g., an object or constant).\\n- There's a syntax error preventing the export from being recognized.\\n\\nTo fix it:\\n- Ensure this file has either a default or \"%s\" function export.\\n\\nLearn more: https://nextjs.org/docs/messages/middleware-to-proxy" } diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index bc01e7e856374..01314d1449e2e 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -2,7 +2,7 @@ import type { NextConfig } from '../../server/config-shared' import type { RouteHas } from '../../lib/load-custom-routes' import { promises as fs } from 'fs' -import { basename } from 'path' +import { relative } from 'path' import { LRUCache } from '../../server/lib/lru-cache' import { extractExportedConstValue, @@ -329,7 +329,7 @@ function validateMiddlewareProxyExports({ return } - const fileName = isProxy ? 'proxy' : 'middleware' + const fileName = isProxy ? PROXY_FILENAME : MIDDLEWARE_FILENAME // Parse AST to get export info (since checkExports doesn't return middleware/proxy info) let hasDefaultExport = false @@ -396,9 +396,22 @@ function validateMiddlewareProxyExports({ (isMiddleware && hasMiddlewareExport) || (isProxy && hasProxyExport) + const relativeFilePath = `./${relative(process.cwd(), pageFilePath)}` + if (!hasValidExport) { throw new Error( - `The ${fileName === 'proxy' ? 'Proxy' : 'Middleware'} file "./${basename(pageFilePath)}" must export a function named \`${fileName}\` or a default function.` + `The file "${relativeFilePath}" must export a function, either as a default export or as a named "${fileName}" export.\n` + + `This function is what Next.js runs for every request handled by this ${fileName === 'proxy' ? 'proxy (previously called middleware)' : 'middleware'}.\n\n` + + `Why this happens:\n` + + (isProxy + ? "- You are migrating from `middleware` to `proxy`, but haven't updated the exported function.\n" + : '') + + `- The file exists but doesn't export a function.\n` + + `- The export is not a function (e.g., an object or constant).\n` + + `- There's a syntax error preventing the export from being recognized.\n\n` + + `To fix it:\n` + + `- Ensure this file has either a default or "${fileName}" function export.\n\n` + + `Learn more: https://nextjs.org/docs/messages/middleware-to-proxy` ) } } diff --git a/packages/next/src/build/templates/middleware.ts b/packages/next/src/build/templates/middleware.ts index 7526961fe65bc..2637bd64dffaa 100644 --- a/packages/next/src/build/templates/middleware.ts +++ b/packages/next/src/build/templates/middleware.ts @@ -19,8 +19,25 @@ const isProxy = page === '/proxy' || page === '/src/proxy' const handler = (isProxy ? mod.proxy : mod.middleware) || mod.default if (typeof handler !== 'function') { + const fileName = isProxy ? 'proxy' : 'middleware' + // Webpack starts the path with "." as relative, but Turbopack does not. + const resolvedRelativeFilePath = relativeFilePath.startsWith('.') + ? relativeFilePath + : `./${relativeFilePath}` + throw new Error( - `The ${isProxy ? 'Proxy' : 'Middleware'} file "${relativeFilePath.startsWith('.') ? relativeFilePath : `./${relativeFilePath}`}" must export a function named \`${isProxy ? 'proxy' : 'middleware'}\` or a default function.` + `The file "${resolvedRelativeFilePath}" must export a function, either as a default export or as a named "${fileName}" export.\n` + + `This function is what Next.js runs for every request handled by this ${fileName === 'proxy' ? 'proxy (previously called middleware)' : 'middleware'}.\n\n` + + `Why this happens:\n` + + (isProxy + ? "- You are migrating from `middleware` to `proxy`, but haven't updated the exported function.\n" + : '') + + `- The file exists but doesn't export a function.\n` + + `- The export is not a function (e.g., an object or constant).\n` + + `- There's a syntax error preventing the export from being recognized.\n\n` + + `To fix it:\n` + + `- Ensure this file has either a default or "${fileName}" function export.\n\n` + + `Learn more: https://nextjs.org/docs/messages/middleware-to-proxy` ) } diff --git a/test/e2e/app-dir/proxy-missing-export/proxy-missing-export.test.ts b/test/e2e/app-dir/proxy-missing-export/proxy-missing-export.test.ts index 61813a45ee11c..92faf1c94e5b2 100644 --- a/test/e2e/app-dir/proxy-missing-export/proxy-missing-export.test.ts +++ b/test/e2e/app-dir/proxy-missing-export/proxy-missing-export.test.ts @@ -2,8 +2,18 @@ import { nextTestSetup } from 'e2e-utils' import { join } from 'node:path' import { writeFile } from 'node:fs/promises' -const errorMessage = - 'The Proxy file "./proxy.ts" must export a function named `proxy` or a default function.' +const errorMessage = `This function is what Next.js runs for every request handled by this proxy (previously called middleware). + +Why this happens: +- You are migrating from \`middleware\` to \`proxy\`, but haven't updated the exported function. +- The file exists but doesn't export a function. +- The export is not a function (e.g., an object or constant). +- There's a syntax error preventing the export from being recognized. + +To fix it: +- Ensure this file has either a default or "proxy" function export. + +Learn more: https://nextjs.org/docs/messages/middleware-to-proxy` describe('proxy-missing-export', () => { const { next, isNextDev, skipped } = nextTestSetup({ @@ -22,14 +32,26 @@ describe('proxy-missing-export', () => { 'export function middleware() {}' ) + let cliOutput: string + if (isNextDev) { await next.start().catch(() => {}) // Use .catch() because Turbopack errors during compile and exits before runtime. await next.browser('/').catch(() => {}) - expect(next.cliOutput).toContain(errorMessage) + cliOutput = next.cliOutput + } else { + cliOutput = (await next.build()).cliOutput + } + + // TODO: Investigate why in dev-turbo, the error is shown in the browser console, not CLI output. + if (process.env.IS_TURBOPACK_TEST && !isNextDev) { + expect(cliOutput).toContain(`./proxy.ts +Proxy is missing expected function export name +${errorMessage}`) } else { - const { cliOutput } = await next.build() - expect(cliOutput).toContain(errorMessage) + expect(cliOutput) + .toContain(`The file "./proxy.ts" must export a function, either as a default export or as a named "proxy" export. +${errorMessage}`) } await next.stop() @@ -94,16 +116,27 @@ describe('proxy-missing-export', () => { 'const proxy = () => {}; export { proxy as handler };' ) + let cliOutput: string + if (isNextDev) { await next.start().catch(() => {}) // Use .catch() because Turbopack errors during compile and exits before runtime. await next.browser('/').catch(() => {}) - expect(next.cliOutput).toContain(errorMessage) + cliOutput = next.cliOutput } else { - const { cliOutput } = await next.build() - expect(cliOutput).toContain(errorMessage) + cliOutput = (await next.build()).cliOutput } + // TODO: Investigate why in dev-turbo, the error is shown in the browser console, not CLI output. + if (process.env.IS_TURBOPACK_TEST && !isNextDev) { + expect(cliOutput).toContain(`./proxy.ts +Proxy is missing expected function export name +${errorMessage}`) + } else { + expect(cliOutput) + .toContain(`The file "./proxy.ts" must export a function, either as a default export or as a named "proxy" export. +${errorMessage}`) + } await next.stop() }) })