diff --git a/Cargo.lock b/Cargo.lock index efa500f3d8c072..cca56b629f9643 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4322,7 +4322,6 @@ dependencies = [ "next-custom-transforms", "next-taskless", "once_cell", - "pathdiff", "percent-encoding", "qstring", "react_remove_properties", diff --git a/crates/next-api/src/middleware.rs b/crates/next-api/src/middleware.rs index be41bce9a4884f..97b2a9438f6b43 100644 --- a/crates/next-api/src/middleware.rs +++ b/crates/next-api/src/middleware.rs @@ -6,8 +6,7 @@ use next_core::{ middleware::get_middleware_module, next_edge::entry::wrap_edge_entry, next_manifests::{EdgeFunctionDefinition, MiddlewaresManifestV2, ProxyMatcher, Regions}, - parse_segment_config_from_source, - segment_config::ParseSegmentMode, + segment_config::NextSegmentConfig, util::{MiddlewareMatcherKind, NextRuntime}, }; use tracing::Instrument; @@ -49,6 +48,8 @@ pub struct MiddlewareEndpoint { source: ResolvedVc>, app_dir: Option, ecmascript_client_reference_transition_name: Option, + config: ResolvedVc, + runtime: NextRuntime, } #[turbo_tasks::value_impl] @@ -60,6 +61,8 @@ impl MiddlewareEndpoint { source: ResolvedVc>, app_dir: Option, ecmascript_client_reference_transition_name: Option, + config: ResolvedVc, + runtime: NextRuntime, ) -> Vc { Self { project, @@ -67,6 +70,8 @@ impl MiddlewareEndpoint { source, app_dir, ecmascript_client_reference_transition_name, + config, + runtime, } .cell() } @@ -81,20 +86,20 @@ impl MiddlewareEndpoint { ) .module(); + let userland_path = userland_module.ident().path().await?; + let is_proxy = userland_path.file_stem() == Some("proxy"); + let module = get_middleware_module( *self.asset_context, self.project.project_path().owned().await?, userland_module, + is_proxy, ); - let runtime = parse_segment_config_from_source(*self.source, ParseSegmentMode::Base) - .await? - .runtime - .unwrap_or(NextRuntime::Edge); - - if matches!(runtime, NextRuntime::NodeJs) { + if matches!(self.runtime, NextRuntime::NodeJs) { return Ok(module); } + Ok(wrap_edge_entry( *self.asset_context, self.project.project_path().owned().await?, @@ -152,10 +157,7 @@ impl MiddlewareEndpoint { #[turbo_tasks::function] async fn output_assets(self: Vc) -> Result> { let this = self.await?; - - let config = - parse_segment_config_from_source(*self.await?.source, ParseSegmentMode::Base).await?; - let runtime = config.runtime.unwrap_or(NextRuntime::Edge); + let config = this.config.await?; let next_config = this.project.next_config(); let i18n = next_config.i18n().await?; @@ -223,7 +225,7 @@ impl MiddlewareEndpoint { }] }; - if matches!(runtime, NextRuntime::NodeJs) { + if matches!(this.runtime, NextRuntime::NodeJs) { let chunk = self.node_chunk().to_resolved().await?; let mut output_assets = vec![chunk]; if this.project.next_mode().await?.is_production() { diff --git a/crates/next-api/src/project.rs b/crates/next-api/src/project.rs index 9193d6d2887386..5258e947d6dde7 100644 --- a/crates/next-api/src/project.rs +++ b/crates/next-api/src/project.rs @@ -1437,28 +1437,6 @@ impl Project { ))) } - #[turbo_tasks::function] - async fn middleware_context(self: Vc) -> Result>> { - let edge_module_context = self.edge_middleware_context(); - - let middleware = self.find_middleware(); - let FindContextFileResult::Found(fs_path, _) = &*middleware.await? else { - return Ok(edge_module_context); - }; - let source = Vc::upcast(FileSource::new(fs_path.clone())); - - let runtime = parse_segment_config_from_source(source, ParseSegmentMode::Base) - .await? - .runtime - .unwrap_or(NextRuntime::Edge); - - if matches!(runtime, NextRuntime::NodeJs) { - Ok(self.node_middleware_context()) - } else { - Ok(edge_module_context) - } - } - #[turbo_tasks::function] async fn find_middleware(self: Vc) -> Result> { Ok(find_context_file( @@ -1483,7 +1461,25 @@ impl Project { .as_ref() .map(|_| AppProject::client_transition_name()); - let middleware_asset_context = self.middleware_context(); + let is_proxy = fs_path.file_stem() == Some("proxy"); + let config = parse_segment_config_from_source( + source, + if is_proxy { + ParseSegmentMode::Proxy + } else { + ParseSegmentMode::Base + }, + ); + let runtime = config.await?.runtime.unwrap_or(if is_proxy { + NextRuntime::NodeJs + } else { + NextRuntime::Edge + }); + + let middleware_asset_context = match runtime { + NextRuntime::NodeJs => self.node_middleware_context(), + NextRuntime::Edge => self.edge_middleware_context(), + }; Ok(Vc::upcast(MiddlewareEndpoint::new( self, @@ -1491,6 +1487,8 @@ impl Project { source, app_dir.clone(), ecmascript_client_reference_transition_name, + config, + runtime, ))) } diff --git a/crates/next-core/src/middleware.rs b/crates/next-core/src/middleware.rs index 56845fd72accf2..51cd7c290caae8 100644 --- a/crates/next-core/src/middleware.rs +++ b/crates/next-core/src/middleware.rs @@ -32,12 +32,12 @@ pub async fn get_middleware_module( asset_context: Vc>, project_root: FileSystemPath, userland_module: ResolvedVc>, + is_proxy: bool, ) -> Result>> { const INNER: &str = "INNER_MIDDLEWARE_MODULE"; // Determine if this is a proxy file by checking the module path let userland_path = userland_module.ident().path().await?; - let is_proxy = userland_path.file_stem() == Some("proxy"); let (file_type, function_name, page_path) = if is_proxy { ("Proxy", "proxy", "/proxy") } else { @@ -91,11 +91,7 @@ pub async fn get_middleware_module( let source = load_next_js_template( "middleware.js", project_root, - &[ - ("VAR_USERLAND", INNER), - ("VAR_DEFINITION_PAGE", page_path), - ("VAR_MODULE_RELATIVE_PATH", userland_path.path.as_str()), - ], + &[("VAR_USERLAND", INNER), ("VAR_DEFINITION_PAGE", page_path)], &[], &[], ) diff --git a/crates/next-core/src/segment_config.rs b/crates/next-core/src/segment_config.rs index 9baff06482b1c2..227012f894a165 100644 --- a/crates/next-core/src/segment_config.rs +++ b/crates/next-core/src/segment_config.rs @@ -289,6 +289,8 @@ pub enum ParseSegmentMode { Base, // Disallows "use client + generateStatic" and ignores/warns about `export const config` App, + // Disallows config = { runtime: "edge" } + Proxy, } /// Parse the raw source code of a file to get the segment config local to that file. @@ -667,21 +669,35 @@ async fn parse_config_value( .await; }; - config.runtime = - match serde_json::from_value(Value::String(val.to_string())) { - Ok(runtime) => Some(runtime), - Err(err) => { - return invalid_config( - source, - "config", - span, - format!("`runtime` has an invalid value: {err}.").into(), - Some(value), - IssueSeverity::Error, - ) - .await; - } - }; + let runtime = match serde_json::from_value(Value::String(val.to_string())) { + Ok(runtime) => Some(runtime), + Err(err) => { + return invalid_config( + source, + "config", + span, + format!("`runtime` has an invalid value: {err}.").into(), + Some(value), + IssueSeverity::Error, + ) + .await; + } + }; + + if mode == ParseSegmentMode::Proxy && runtime == Some(NextRuntime::Edge) { + invalid_config( + source, + "config", + span, + rcstr!("Proxy does not support Edge runtime."), + Some(value), + IssueSeverity::Error, + ) + .await?; + continue; + } + + config.runtime = runtime } "matcher" => { config.middleware_matcher = diff --git a/packages/next/errors.json b/packages/next/errors.json index cbe2d5256c427d..fe9759e8d65ee5 100644 --- a/packages/next/errors.json +++ b/packages/next/errors.json @@ -901,5 +901,6 @@ "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", - "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" + "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", + "904": "The file \"%s\" must export a function, either as a default export or as a named \"%s\" export." } 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 01314d1449e2e3..fe1fae37a44602 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -40,6 +40,7 @@ import { } from '../segment-config/middleware/middleware-config' import { normalizeAppPath } from '../../shared/lib/router/utils/app-paths' import { normalizePagePath } from '../../shared/lib/page-path/normalize-page-path' +import { isProxyFile } from '../utils' const PARSE_PATTERN = /(? - isDev?: boolean + isDev: boolean page: string pageType: PAGE_TYPES } @@ -617,7 +625,12 @@ export async function getAppPageStaticInfo({ } const ast = await parseModule(pageFilePath, content) - validateMiddlewareProxyExports({ ast, page, pageFilePath }) + validateMiddlewareProxyExports({ + ast, + page, + pageFilePath, + isDev, + }) const { generateStaticParams, @@ -713,7 +726,12 @@ export async function getPagesPageStaticInfo({ } const ast = await parseModule(pageFilePath, content) - validateMiddlewareProxyExports({ ast, page, pageFilePath }) + validateMiddlewareProxyExports({ + ast, + page, + pageFilePath, + isDev, + }) const { getServerSideProps, getStaticProps, exports } = checkExports( ast, @@ -750,13 +768,35 @@ export async function getPagesPageStaticInfo({ const config = parsePagesSegmentConfig(exportedConfig, route) const isAnAPIRoute = isAPIRoute(route) - const resolvedRuntime = config.runtime ?? config.config?.runtime + let resolvedRuntime = config.runtime ?? config.config?.runtime + + if (isProxyFile(page) && resolvedRuntime) { + const relativePath = relative(process.cwd(), pageFilePath) + const resolvedPath = relativePath.startsWith('.') + ? relativePath + : `./${relativePath}` + const message = `Route segment config is not allowed in Proxy file at "${resolvedPath}". Proxy always runs on Node.js runtime. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy` + + if (isDev) { + // errorOnce as proxy/middleware runs per request including multiple + // internal _next/ routes and spams logs. + Log.errorOnce(message) + resolvedRuntime = SERVER_RUNTIME.nodejs + } else { + throw new Error(message) + } + } if (resolvedRuntime === SERVER_RUNTIME.experimentalEdge) { warnAboutExperimentalEdge(isAnAPIRoute ? page! : null) } - if (resolvedRuntime === SERVER_RUNTIME.edge && page && !isAnAPIRoute) { + if ( + !isProxyFile(page) && + resolvedRuntime === SERVER_RUNTIME.edge && + page && + !isAnAPIRoute + ) { const message = `Page ${page} provided runtime 'edge', the edge runtime for rendering is currently experimental. Use runtime 'experimental-edge' instead.` if (isDev) { Log.error(message) diff --git a/packages/next/src/build/entries.ts b/packages/next/src/build/entries.ts index 50a4762661e5c6..7f1f17f53784a1 100644 --- a/packages/next/src/build/entries.ts +++ b/packages/next/src/build/entries.ts @@ -20,8 +20,6 @@ import { APP_DIR_ALIAS, WEBPACK_LAYERS, INSTRUMENTATION_HOOK_FILENAME, - PROXY_FILENAME, - MIDDLEWARE_FILENAME, } from '../lib/constants' import { isAPIRoute } from '../lib/is-api-route' import { isEdgeRuntime } from '../lib/is-edge-runtime' @@ -43,6 +41,7 @@ import type { __ApiPreviewProps } from '../server/api-utils' import { isMiddlewareFile, isMiddlewareFilename, + isProxyFile, isInstrumentationHookFile, isInstrumentationHookFilename, } from './utils' @@ -573,7 +572,7 @@ export interface CreateEntrypointsParams { buildId: string config: NextConfigComplete envFiles: LoadedEnvFiles - isDev?: boolean + isDev: boolean pages: MappedPages pagesDir?: string previewMode: __ApiPreviewProps @@ -642,6 +641,7 @@ export function getEdgeServerEntry(opts: { return { import: `next-middleware-loader?${stringify(loaderParams)}!`, layer: WEBPACK_LAYERS.middleware, + filename: opts.isDev ? 'middleware.js' : undefined, } } @@ -764,6 +764,11 @@ export function runDependingOnPageType(params: { return } + if (isProxyFile(params.page)) { + params.onServer() + return + } + if (isMiddlewareFile(params.page)) { if (params.pageRuntime === 'nodejs') { params.onServer() @@ -947,13 +952,7 @@ export async function createEntrypoints( isDev: false, }) } else if (isMiddlewareFile(page)) { - server[ - serverBundlePath - // proxy.js still uses middleware.js for bundle path for now. - // TODO: Revisit when we remove middleware.js. - .replace(PROXY_FILENAME, MIDDLEWARE_FILENAME) - .replace('src/', '') - ] = getEdgeServerEntry({ + server[serverBundlePath.replace('src/', '')] = getEdgeServerEntry({ ...params, rootDir, absolutePagePath: absolutePagePath, @@ -1028,12 +1027,7 @@ export async function createEntrypoints( : undefined, }).import } - const edgeServerBundlePath = isMiddlewareFile(page) - ? serverBundlePath - .replace(PROXY_FILENAME, MIDDLEWARE_FILENAME) - .replace('src/', '') - : serverBundlePath - edgeServer[edgeServerBundlePath] = getEdgeServerEntry({ + edgeServer[serverBundlePath] = getEdgeServerEntry({ ...params, rootDir, absolutePagePath: absolutePagePath, diff --git a/packages/next/src/build/get-static-info-including-layouts.ts b/packages/next/src/build/get-static-info-including-layouts.ts index cf04c553749318..260bccb115ff43 100644 --- a/packages/next/src/build/get-static-info-including-layouts.ts +++ b/packages/next/src/build/get-static-info-including-layouts.ts @@ -29,7 +29,7 @@ export async function getStaticInfoIncludingLayouts({ pageFilePath: string appDir: string | undefined config: NextConfigComplete - isDev: boolean | undefined + isDev: boolean page: string }): Promise { // TODO: sync types for pages: PAGE_TYPES, ROUTER_TYPE, 'app' | 'pages', etc. diff --git a/packages/next/src/build/index.ts b/packages/next/src/build/index.ts index bb52c5db5984f3..c9e6b1326473f8 100644 --- a/packages/next/src/build/index.ts +++ b/packages/next/src/build/index.ts @@ -139,6 +139,7 @@ import { isAppBuiltinPage, collectRoutesUsingEdgeRuntime, collectMeta, + isProxyFile, } from './utils' import type { PageInfo, PageInfos } from './utils' import type { FallbackRouteParam, PrerenderedRoute } from './static-paths/types' @@ -1214,13 +1215,7 @@ export default async function build( for (const rootPath of rootPaths) { const { name: fileBaseName, dir: fileDir } = path.parse(rootPath) - const isAtConventionLevel = - fileDir === '/' || - fileDir === '/src' || - // rootPaths are currently relative paths from the root directory. - // Add safety check here for unexpected future changes. - fileDir === dir || - fileDir === path.join(dir, 'src') + const isAtConventionLevel = fileDir === '/' || fileDir === '/src' if (isAtConventionLevel && fileBaseName === MIDDLEWARE_FILENAME) { middlewareFilePath = rootPath @@ -2615,12 +2610,15 @@ export default async function build( return serverFilesManifest }) - const middlewareFile = rootPaths.find( - (p) => p.includes(MIDDLEWARE_FILENAME) || p.includes(PROXY_FILENAME) - ) + const middlewareFile = proxyFilePath || middlewareFilePath let hasNodeMiddleware = false if (middlewareFile) { + // Is format of `(/src)/(proxy|middleware).`, so split by + // "." and get the first part, regard rest of the extensions + // to match the `page` value format. + const page = middlewareFile.split('.')[0] + const staticInfo = await getStaticInfoIncludingLayouts({ isInsideAppDir: false, pageFilePath: path.join(dir, middlewareFile), @@ -2628,17 +2626,17 @@ export default async function build( appDir, pageExtensions: config.pageExtensions, isDev: false, - page: 'middleware', + page, }) if (staticInfo.hadUnsupportedValue) { errorFromUnsupportedSegmentConfig() } - if (staticInfo.runtime === 'nodejs') { + if (staticInfo.runtime === 'nodejs' || isProxyFile(page)) { hasNodeMiddleware = true functionsConfigManifest.functions['/_middleware'] = { - runtime: staticInfo.runtime, + runtime: 'nodejs', matchers: staticInfo.middleware?.matchers ?? [ { regexp: '^.*$', @@ -4154,6 +4152,17 @@ export default async function build( buildTracesSpinner = undefined } + if (proxyFilePath && bundler !== Bundler.Turbopack) { + await fs.rename( + path.join(distDir, SERVER_DIRECTORY, 'proxy.js'), + path.join(distDir, SERVER_DIRECTORY, 'middleware.js') + ) + await fs.rename( + path.join(distDir, SERVER_DIRECTORY, 'proxy.js.nft.json'), + path.join(distDir, SERVER_DIRECTORY, 'middleware.js.nft.json') + ) + } + if (isCompileMode) { Log.info( `Build ran with "compile" mode, to finalize the build run either "generate" or "generate-env" mode as well` diff --git a/packages/next/src/build/output/log.ts b/packages/next/src/build/output/log.ts index b62c42316dcc63..41ca0dc2543818 100644 --- a/packages/next/src/build/output/log.ts +++ b/packages/next/src/build/output/log.ts @@ -85,3 +85,12 @@ export function warnOnce(...message: any[]) { warn(...message) } } + +const errorOnceCache = new LRUCache(10_000, (value) => value.length) +export function errorOnce(...message: any[]) { + const key = message.join(' ') + if (!errorOnceCache.has(key)) { + errorOnceCache.set(key, key) + error(...message) + } +} diff --git a/packages/next/src/build/templates/middleware.ts b/packages/next/src/build/templates/middleware.ts index 2637bd64dffaab..599b432b083e85 100644 --- a/packages/next/src/build/templates/middleware.ts +++ b/packages/next/src/build/templates/middleware.ts @@ -11,33 +11,23 @@ import { isNextRouterError } from '../../client/components/is-next-router-error' const mod = { ..._mod } -const page = 'VAR_DEFINITION_PAGE' -// Turbopack does not add a `./` prefix to the relative file path, but Webpack does. -const relativeFilePath = 'VAR_MODULE_RELATIVE_PATH' -// @ts-expect-error `page` will be replaced during build +const page: string = 'VAR_DEFINITION_PAGE' 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}` +class ProxyMissingExportError extends Error { + constructor(message: string) { + super(message) + // Stack isn't useful here, remove it considering it spams logs during development. + this.stack = '' + } +} - throw new Error( - `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` +// TODO: This spams logs during development. Find a better way to handle this. +// Removing this will spam "fn is not a function" logs which is worse. +if (typeof handler !== 'function') { + throw new ProxyMissingExportError( + `The ${isProxy ? 'Proxy' : 'Middleware'} file "${page}" must export a function named \`${isProxy ? 'proxy' : 'middleware'}\` or a default function.` ) } diff --git a/packages/next/src/build/utils.ts b/packages/next/src/build/utils.ts index 78fedfee3a1ff2..01454bb2ffb614 100644 --- a/packages/next/src/build/utils.ts +++ b/packages/next/src/build/utils.ts @@ -1421,6 +1421,10 @@ export function isMiddlewareFile(file: string) { ) } +export function isProxyFile(file: string) { + return file === `/${PROXY_FILENAME}` || file === `/src/${PROXY_FILENAME}` +} + export function isInstrumentationHookFile(file: string) { return ( file === `/${INSTRUMENTATION_HOOK_FILENAME}` || diff --git a/packages/next/src/build/webpack/loaders/next-middleware-loader.ts b/packages/next/src/build/webpack/loaders/next-middleware-loader.ts index 512d1ca32d05c6..9f2066f3a4c29d 100644 --- a/packages/next/src/build/webpack/loaders/next-middleware-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-middleware-loader.ts @@ -70,8 +70,5 @@ export default async function middlewareLoader(this: any) { return await loadEntrypoint('middleware', { VAR_USERLAND: pagePath, VAR_DEFINITION_PAGE: page, - // Turbopack sets `VAR_USERLAND` to `INNER_MIDDLEWARE_MODULE`, so use - // `VAR_MODULE_RELATIVE_PATH` for error messages. - VAR_MODULE_RELATIVE_PATH: pagePath, }) } diff --git a/packages/next/src/server/dev/hot-reloader-turbopack.ts b/packages/next/src/server/dev/hot-reloader-turbopack.ts index caa6ddbc3c5ca2..d9c027e143cd4d 100644 --- a/packages/next/src/server/dev/hot-reloader-turbopack.ts +++ b/packages/next/src/server/dev/hot-reloader-turbopack.ts @@ -89,6 +89,7 @@ import { processIssues, renderStyledStringToErrorAnsi, type EntryIssuesMap, + type IssuesMap, type TopLevelIssuesMap, } from '../../shared/lib/turbopack/utils' import { getDevOverlayFontMiddleware } from '../../next-devtools/server/font/get-dev-overlay-font-middleware' @@ -1432,28 +1433,36 @@ export async function createHotReloaderTurbopack( case 'end': { sendEnqueuedMessages() + function addToErrorsMap( + errorsMap: Map, + issueMap: IssuesMap + ) { + for (const [key, issue] of issueMap) { + if (issue.severity === 'warning') continue + if (errorsMap.has(key)) continue + + const message = formatIssue(issue) + + errorsMap.set(key, { + message, + details: issue.detail + ? renderStyledStringToErrorAnsi(issue.detail) + : undefined, + }) + } + } + function addErrors( errorsMap: Map, issues: EntryIssuesMap ) { for (const issueMap of issues.values()) { - for (const [key, issue] of issueMap) { - if (issue.severity === 'warning') continue - if (errorsMap.has(key)) continue - - const message = formatIssue(issue) - - errorsMap.set(key, { - message, - details: issue.detail - ? renderStyledStringToErrorAnsi(issue.detail) - : undefined, - }) - } + addToErrorsMap(errorsMap, issueMap) } } const errors = new Map() + addToErrorsMap(errors, currentTopLevelIssues) addErrors(errors, currentEntryIssues) for (const client of [ diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index 3e0adebee88425..8616e7326612b8 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -1574,6 +1574,10 @@ export default class NextNodeServer extends BaseServer< functionsConfig?.functions?.['/_middleware'] ) { // if used with top level await, this will be a promise + // Try loading middleware.js first, then proxy.js. Instead + // of mapping proxy to middleware as the entry, just fallback + // to proxy. + // TODO: Remove this once we handle as the single entrypoint. return require( join( /* turbopackIgnore: true */ this.distDir, @@ -1736,7 +1740,10 @@ export default class NextNodeServer extends BaseServer< try { result = await adapterFn({ - handler: middlewareModule.middleware || middlewareModule, + handler: + middlewareModule.proxy || + middlewareModule.middleware || + middlewareModule, request: { ...requestData, body: hasRequestBody diff --git a/test/e2e/app-dir/app-middleware-proxy/app-middleware-proxy.test.ts b/test/e2e/app-dir/app-middleware-proxy/app-middleware-proxy.test.ts index b0c080aadbe9a8..35c19a0f4e0e8c 100644 --- a/test/e2e/app-dir/app-middleware-proxy/app-middleware-proxy.test.ts +++ b/test/e2e/app-dir/app-middleware-proxy/app-middleware-proxy.test.ts @@ -117,7 +117,9 @@ describe('app-dir with proxy', () => { expect(res.headers.get('x-middleware-request-x-from-client3')).toBeNull() }) - it(`Supports draft mode`, async () => { + // Cannot set draftMode in nodejs runtime + // TODO: Investigate https://github.com/vercel/next.js/pull/85174 + it.skip(`Supports draft mode`, async () => { const res = await next.fetch(`${path}?draft=true`) const headers: string = res.headers.get('set-cookie') || '' const bypassCookie = headers diff --git a/test/e2e/app-dir/app-middleware-proxy/proxy.js b/test/e2e/app-dir/app-middleware-proxy/proxy.js index d5a7c82b381ad3..58ab998be71863 100644 --- a/test/e2e/app-dir/app-middleware-proxy/proxy.js +++ b/test/e2e/app-dir/app-middleware-proxy/proxy.js @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server' -import { headers as nextHeaders, draftMode } from 'next/headers' +import { headers as nextHeaders } from 'next/headers' /** * @param {import('next/server').NextRequest} request @@ -20,9 +20,11 @@ export async function proxy(request) { throw new Error('Expected headers from client to match') } - if (request.nextUrl.searchParams.get('draft')) { - ;(await draftMode()).enable() - } + // Cannot set draftMode in nodejs runtime + // TODO: Investigate https://github.com/vercel/next.js/pull/85174 + // if (request.nextUrl.searchParams.get('draft')) { + // ;(await draftMode()).enable() + // } const removeHeaders = request.nextUrl.searchParams.get('remove-headers') if (removeHeaders) { diff --git a/test/e2e/app-dir/proxy-runtime-nodejs/app/layout.tsx b/test/e2e/app-dir/proxy-runtime-nodejs/app/layout.tsx new file mode 100644 index 00000000000000..888614deda3ba5 --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime-nodejs/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/proxy-runtime-nodejs/app/page.tsx b/test/e2e/app-dir/proxy-runtime-nodejs/app/page.tsx new file mode 100644 index 00000000000000..ff7159d9149fee --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime-nodejs/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/app-dir/proxy-runtime-nodejs/next.config.js b/test/e2e/app-dir/proxy-runtime-nodejs/next.config.js new file mode 100644 index 00000000000000..807126e4cf0bf5 --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime-nodejs/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/proxy-runtime-nodejs/proxy-runtime-nodejs.test.ts b/test/e2e/app-dir/proxy-runtime-nodejs/proxy-runtime-nodejs.test.ts new file mode 100644 index 00000000000000..f68f7377ecb757 --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime-nodejs/proxy-runtime-nodejs.test.ts @@ -0,0 +1,12 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('proxy-runtime-nodejs', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should use nodejs runtime for proxy by default', async () => { + const browser = await next.browser('/foo') + expect(await browser.elementByCss('p').text()).toBe('hello world') + }) +}) diff --git a/test/e2e/app-dir/proxy-runtime-nodejs/proxy.ts b/test/e2e/app-dir/proxy-runtime-nodejs/proxy.ts new file mode 100644 index 00000000000000..dd49dfb801a3d8 --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime-nodejs/proxy.ts @@ -0,0 +1,11 @@ +import { NextRequest, NextResponse } from 'next/server' +// Will not work in edge runtime +import { join } from 'path/posix' + +export function proxy(request: NextRequest) { + if (request.nextUrl.pathname === join('/', 'foo')) { + return NextResponse.redirect(new URL('/', request.url)) + } + + return NextResponse.next() +} diff --git a/test/e2e/app-dir/proxy-runtime-nodejs/redirect-path.txt b/test/e2e/app-dir/proxy-runtime-nodejs/redirect-path.txt new file mode 100644 index 00000000000000..7370610cce702d --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime-nodejs/redirect-path.txt @@ -0,0 +1 @@ +/foo \ No newline at end of file diff --git a/test/e2e/app-dir/proxy-runtime/app/layout.tsx b/test/e2e/app-dir/proxy-runtime/app/layout.tsx new file mode 100644 index 00000000000000..888614deda3ba5 --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/proxy-runtime/app/page.tsx b/test/e2e/app-dir/proxy-runtime/app/page.tsx new file mode 100644 index 00000000000000..ff7159d9149fee --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/app-dir/proxy-runtime/next.config.js b/test/e2e/app-dir/proxy-runtime/next.config.js new file mode 100644 index 00000000000000..807126e4cf0bf5 --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts b/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts new file mode 100644 index 00000000000000..ca90133c084337 --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime/proxy-runtime.test.ts @@ -0,0 +1,46 @@ +import { nextTestSetup } from 'e2e-utils' +import stripAnsi from 'strip-ansi' + +describe('proxy-runtime', () => { + const { next, isNextDev, skipped } = nextTestSetup({ + files: __dirname, + skipDeployment: true, + skipStart: true, + }) + + if (skipped) { + return + } + + it('should error when proxy file has runtime config export', async () => { + let cliOutput: string + + if (isNextDev) { + await next.start().catch(() => {}) + // Use .catch() because Turbopack errors during compile and exits before runtime. + await next.browser('/').catch(() => {}) + 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(stripAnsi(cliOutput)).toContain(`proxy.ts:3:14 +Next.js can't recognize the exported \`config\` field in route. Proxy does not support Edge runtime. + 1 | export default function () {} + 2 | +> 3 | export const config = { runtime: 'edge' } + | ^^^^^^ + 4 | + +The exported configuration object in a source file needs to have a very specific format from which some properties can be statically parsed at compiled-time.`) + } else { + expect(cliOutput).toContain( + `Route segment config is not allowed in Proxy file at "./proxy.ts". Proxy always runs on Node.js runtime. Learn more: https://nextjs.org/docs/messages/middleware-to-proxy` + ) + } + + await next.stop() + }) +}) diff --git a/test/e2e/app-dir/proxy-runtime/proxy.ts b/test/e2e/app-dir/proxy-runtime/proxy.ts new file mode 100644 index 00000000000000..586ed05ca9d43a --- /dev/null +++ b/test/e2e/app-dir/proxy-runtime/proxy.ts @@ -0,0 +1,3 @@ +export default function () {} + +export const config = { runtime: 'edge' } diff --git a/test/production/proxy-typescript/app/proxy.ts b/test/production/proxy-typescript/app/proxy.ts index 85db0de0aa0d53..fcdb0047720ba1 100644 --- a/test/production/proxy-typescript/app/proxy.ts +++ b/test/production/proxy-typescript/app/proxy.ts @@ -1,11 +1,6 @@ -import { NextProxy, NextResponse, URLPattern, ProxyConfig } from 'next/server' +import { NextProxy, NextResponse, ProxyConfig } from 'next/server' export const proxy: NextProxy = function (request) { - const pattern = new URLPattern({ - pathname: '/:path', - }) - console.log(pattern.test(request.nextUrl.pathname)) - if (request.nextUrl.pathname === '/static') { return new NextResponse(null, { headers: { diff --git a/test/unit/parse-page-static-info.test.ts b/test/unit/parse-page-static-info.test.ts index e1282b97336def..df004408407f3d 100644 --- a/test/unit/parse-page-static-info.test.ts +++ b/test/unit/parse-page-static-info.test.ts @@ -16,6 +16,7 @@ describe('parse page static info', () => { pageFilePath: join(fixtureDir, 'page-runtime/nodejs-ssr.js'), nextConfig: createNextConfig(), pageType: PAGE_TYPES.PAGES, + isDev: false, }) expect(runtime).toBe('nodejs') expect(getServerSideProps).toBe(true) @@ -29,6 +30,7 @@ describe('parse page static info', () => { pageFilePath: join(fixtureDir, 'page-runtime/nodejs.js'), nextConfig: createNextConfig(), pageType: PAGE_TYPES.PAGES, + isDev: false, }) expect(runtime).toBe('nodejs') expect(getServerSideProps).toBe(false) @@ -41,6 +43,7 @@ describe('parse page static info', () => { pageFilePath: join(fixtureDir, 'page-runtime/edge.js'), nextConfig: createNextConfig(), pageType: PAGE_TYPES.PAGES, + isDev: false, }) expect(runtime).toBe('experimental-edge') }) @@ -51,6 +54,7 @@ describe('parse page static info', () => { pageFilePath: join(fixtureDir, 'page-runtime/static.js'), nextConfig: createNextConfig(), pageType: PAGE_TYPES.PAGES, + isDev: false, }) expect(runtime).toBe(undefined) }) @@ -62,6 +66,7 @@ describe('parse page static info', () => { pageFilePath: join(fixtureDir, 'page-runtime/ssr-variable-gssp.js'), nextConfig: createNextConfig(), pageType: PAGE_TYPES.PAGES, + isDev: false, } ) expect(getStaticProps).toBe(false)