From d462222bb7633abfecc43fbfdb66c549eec2fca2 Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:52:46 -0700 Subject: [PATCH 1/3] fix: route handlers should validate invalid exports --- .../src/transforms/react_server_components.rs | 16 ++--- .../app/layout.tsx | 11 ++++ .../app/route-with-dynamic/route.ts | 5 ++ .../app/route-with-fetchcache/route.ts | 5 ++ .../app/route-with-revalidate/route.ts | 5 ++ ...he-components-route-handler-errors.test.ts | 65 +++++++++++++++++++ .../next.config.js | 10 +++ 7 files changed, 109 insertions(+), 8 deletions(-) create mode 100644 test/e2e/app-dir/cache-components-route-handler-errors/app/layout.tsx create mode 100644 test/e2e/app-dir/cache-components-route-handler-errors/app/route-with-dynamic/route.ts create mode 100644 test/e2e/app-dir/cache-components-route-handler-errors/app/route-with-fetchcache/route.ts create mode 100644 test/e2e/app-dir/cache-components-route-handler-errors/app/route-with-revalidate/route.ts create mode 100644 test/e2e/app-dir/cache-components-route-handler-errors/cache-components-route-handler-errors.test.ts create mode 100644 test/e2e/app-dir/cache-components-route-handler-errors/next.config.js diff --git a/crates/next-custom-transforms/src/transforms/react_server_components.rs b/crates/next-custom-transforms/src/transforms/react_server_components.rs index 446ab7c30b06c..b34b9c1e9f8fa 100644 --- a/crates/next-custom-transforms/src/transforms/react_server_components.rs +++ b/crates/next-custom-transforms/src/transforms/react_server_components.rs @@ -11,19 +11,19 @@ use regex::Regex; use rustc_hash::FxHashMap; use serde::Deserialize; use swc_core::{ - atoms::{atom, Atom}, + atoms::{Atom, atom}, common::{ + DUMMY_SP, FileName, Span, Spanned, comments::{Comment, CommentKind, Comments}, errors::HANDLER, util::take::Take, - FileName, Span, Spanned, DUMMY_SP, }, ecma::{ ast::*, - utils::{prepend_stmts, quote_ident, quote_str, ExprFactory}, + utils::{ExprFactory, prepend_stmts, quote_ident, quote_str}, visit::{ - noop_visit_mut_type, noop_visit_type, visit_mut_pass, Visit, VisitMut, VisitMutWith, - VisitWith, + Visit, VisitMut, VisitMutWith, VisitWith, noop_visit_mut_type, noop_visit_type, + visit_mut_pass, }, }, }; @@ -805,10 +805,10 @@ impl ReactServerComponentValidator { return; } static RE: Lazy = - Lazy::new(|| Regex::new(r"[\\/](page|layout)\.(ts|js)x?$").unwrap()); - let is_layout_or_page = RE.is_match(&self.filepath); + Lazy::new(|| Regex::new(r"[\\/](page|layout|route)\.(ts|js)x?$").unwrap()); + let is_app_entry = RE.is_match(&self.filepath); - if is_layout_or_page { + if is_app_entry { let mut possibly_invalid_exports: FxIndexMap = FxIndexMap::default(); diff --git a/test/e2e/app-dir/cache-components-route-handler-errors/app/layout.tsx b/test/e2e/app-dir/cache-components-route-handler-errors/app/layout.tsx new file mode 100644 index 0000000000000..dbce4ea8e3aeb --- /dev/null +++ b/test/e2e/app-dir/cache-components-route-handler-errors/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/cache-components-route-handler-errors/app/route-with-dynamic/route.ts b/test/e2e/app-dir/cache-components-route-handler-errors/app/route-with-dynamic/route.ts new file mode 100644 index 0000000000000..af6114a0ef51d --- /dev/null +++ b/test/e2e/app-dir/cache-components-route-handler-errors/app/route-with-dynamic/route.ts @@ -0,0 +1,5 @@ +export const dynamic = 'force-static' + +export async function GET(request: Request) { + return new Response('route GET') +} diff --git a/test/e2e/app-dir/cache-components-route-handler-errors/app/route-with-fetchcache/route.ts b/test/e2e/app-dir/cache-components-route-handler-errors/app/route-with-fetchcache/route.ts new file mode 100644 index 0000000000000..e69ecb83f6260 --- /dev/null +++ b/test/e2e/app-dir/cache-components-route-handler-errors/app/route-with-fetchcache/route.ts @@ -0,0 +1,5 @@ +export const fetchCache = 'force-cache' + +export async function GET(request: Request) { + return new Response('route GET with fetchCache') +} diff --git a/test/e2e/app-dir/cache-components-route-handler-errors/app/route-with-revalidate/route.ts b/test/e2e/app-dir/cache-components-route-handler-errors/app/route-with-revalidate/route.ts new file mode 100644 index 0000000000000..76da65a6e1c4e --- /dev/null +++ b/test/e2e/app-dir/cache-components-route-handler-errors/app/route-with-revalidate/route.ts @@ -0,0 +1,5 @@ +export const revalidate = 60 + +export async function GET(request: Request) { + return new Response('route GET with revalidate') +} diff --git a/test/e2e/app-dir/cache-components-route-handler-errors/cache-components-route-handler-errors.test.ts b/test/e2e/app-dir/cache-components-route-handler-errors/cache-components-route-handler-errors.test.ts new file mode 100644 index 0000000000000..4646d941f01db --- /dev/null +++ b/test/e2e/app-dir/cache-components-route-handler-errors/cache-components-route-handler-errors.test.ts @@ -0,0 +1,65 @@ +import { nextTestSetup } from 'e2e-utils' +import { + assertHasRedbox, + getRedboxDescription, + getRedboxSource, +} from 'next-test-utils' + +describe('cache-components-route-handler-errors', () => { + const { next, skipped, isNextDev, isTurbopack } = nextTestSetup({ + files: __dirname, + skipStart: true, + skipDeployment: true, + }) + + if (skipped) { + return + } + + it("should error when route handlers use segment configs that aren't supported by cacheComponents", async () => { + try { + await next.start() + } catch { + // we expect the build to fail + } + + if (isNextDev) { + // Test the first route handler with "dynamic" config + const browser = await next.browser('/route-with-dynamic') + await assertHasRedbox(browser) + const redbox = { + description: await getRedboxDescription(browser), + source: await getRedboxSource(browser), + } + + if (isTurbopack) { + expect(redbox.description).toMatchInlineSnapshot( + `"Ecmascript file had an error"` + ) + } else { + expect(redbox.description).toMatchInlineSnapshot( + `" x Route segment config "dynamic" is not compatible with \`nextConfig.experimental.cacheComponents\`. Please remove it."` + ) + } + expect(redbox.source).toContain( + '"dynamic" is not compatible with `nextConfig.experimental.cacheComponents`. Please remove it.' + ) + } else { + // In build mode, check for all three errors in the output + expect(next.cliOutput).toContain('./app/route-with-dynamic/route.ts') + expect(next.cliOutput).toContain( + '"dynamic" is not compatible with `nextConfig.experimental.cacheComponents`. Please remove it.' + ) + + expect(next.cliOutput).toContain('./app/route-with-revalidate/route.ts') + expect(next.cliOutput).toContain( + '"revalidate" is not compatible with `nextConfig.experimental.cacheComponents`. Please remove it.' + ) + + expect(next.cliOutput).toContain('./app/route-with-fetchcache/route.ts') + expect(next.cliOutput).toContain( + '"fetchCache" is not compatible with `nextConfig.experimental.cacheComponents`. Please remove it.' + ) + } + }) +}) diff --git a/test/e2e/app-dir/cache-components-route-handler-errors/next.config.js b/test/e2e/app-dir/cache-components-route-handler-errors/next.config.js new file mode 100644 index 0000000000000..30a826fdacc56 --- /dev/null +++ b/test/e2e/app-dir/cache-components-route-handler-errors/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + cacheComponents: true, + }, +} + +module.exports = nextConfig From bae5f9f81e4c4144ca64cb162651c3584e134c1e Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Fri, 5 Sep 2025 13:58:49 -0700 Subject: [PATCH 2/3] lint --- .../src/transforms/react_server_components.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/next-custom-transforms/src/transforms/react_server_components.rs b/crates/next-custom-transforms/src/transforms/react_server_components.rs index b34b9c1e9f8fa..98306014b48cc 100644 --- a/crates/next-custom-transforms/src/transforms/react_server_components.rs +++ b/crates/next-custom-transforms/src/transforms/react_server_components.rs @@ -11,19 +11,19 @@ use regex::Regex; use rustc_hash::FxHashMap; use serde::Deserialize; use swc_core::{ - atoms::{Atom, atom}, + atoms::{atom, Atom}, common::{ - DUMMY_SP, FileName, Span, Spanned, comments::{Comment, CommentKind, Comments}, errors::HANDLER, util::take::Take, + FileName, Span, Spanned, DUMMY_SP, }, ecma::{ ast::*, - utils::{ExprFactory, prepend_stmts, quote_ident, quote_str}, + utils::{prepend_stmts, quote_ident, quote_str, ExprFactory}, visit::{ - Visit, VisitMut, VisitMutWith, VisitWith, noop_visit_mut_type, noop_visit_type, - visit_mut_pass, + noop_visit_mut_type, noop_visit_type, visit_mut_pass, Visit, VisitMut, VisitMutWith, + VisitWith, }, }, }; From 961ca95f91ef334595f2acc643aad7510188d513 Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:11:17 -0700 Subject: [PATCH 3/3] remove invalid test --- .../app/not-instrumented/edge-route-handler/messages.ts | 1 - .../app/not-instrumented/edge-route-handler/route.ts | 9 --------- .../cache-components-dynamic-imports.test.ts | 7 ------- test/experimental-tests-manifest.json | 2 ++ 4 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 test/e2e/app-dir/cache-components-dynamic-imports/bundled/app/not-instrumented/edge-route-handler/messages.ts delete mode 100644 test/e2e/app-dir/cache-components-dynamic-imports/bundled/app/not-instrumented/edge-route-handler/route.ts diff --git a/test/e2e/app-dir/cache-components-dynamic-imports/bundled/app/not-instrumented/edge-route-handler/messages.ts b/test/e2e/app-dir/cache-components-dynamic-imports/bundled/app/not-instrumented/edge-route-handler/messages.ts deleted file mode 100644 index b1e71f1b5ee5d..0000000000000 --- a/test/e2e/app-dir/cache-components-dynamic-imports/bundled/app/not-instrumented/edge-route-handler/messages.ts +++ /dev/null @@ -1 +0,0 @@ -export default { title: 'hello' } diff --git a/test/e2e/app-dir/cache-components-dynamic-imports/bundled/app/not-instrumented/edge-route-handler/route.ts b/test/e2e/app-dir/cache-components-dynamic-imports/bundled/app/not-instrumented/edge-route-handler/route.ts deleted file mode 100644 index c7ca93ac75f44..0000000000000 --- a/test/e2e/app-dir/cache-components-dynamic-imports/bundled/app/not-instrumented/edge-route-handler/route.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const runtime = 'edge' - -export async function GET(_request: Request) { - // This import should not be instrumented, because edge routes are never prerendered. - // `trackDynamicImport` will throw if it's used in the edge runtime, - // so it's enough to just do an import() here and see if it succeeds. - const messages = (await import('./messages')).default - return new Response(messages.title) -} diff --git a/test/e2e/app-dir/cache-components-dynamic-imports/cache-components-dynamic-imports.test.ts b/test/e2e/app-dir/cache-components-dynamic-imports/cache-components-dynamic-imports.test.ts index 07e67d4cad7d6..8a0005c0aba44 100644 --- a/test/e2e/app-dir/cache-components-dynamic-imports/cache-components-dynamic-imports.test.ts +++ b/test/e2e/app-dir/cache-components-dynamic-imports/cache-components-dynamic-imports.test.ts @@ -161,13 +161,6 @@ describe('async imports in cacheComponents', () => { // indirectly tests the behavior of middleware by rendering a page which the middleware matches await testPage('/not-instrumented/middleware') }) - - it('edge route handler', async () => { - const result = await next - .fetch('/not-instrumented/edge-route-handler') - .then((res) => res.text()) - expect(result).toBe('hello') - }) }) }) diff --git a/test/experimental-tests-manifest.json b/test/experimental-tests-manifest.json index 75ff2169af9b6..b51a0334f73b9 100644 --- a/test/experimental-tests-manifest.json +++ b/test/experimental-tests-manifest.json @@ -140,6 +140,7 @@ "test/e2e/app-dir/app/standalone-gsp.test.ts", "test/e2e/app-dir/app/standalone.test.ts", "test/e2e/app-dir/app/useReportWebVitals.test.ts", + "test/e2e/app-dir/bun-externals/bun-externals.test.ts", "test/e2e/app-dir/async-component-preload/async-component-preload.test.ts", "test/e2e/app-dir/autoscroll-with-css-modules/index.test.ts", "test/e2e/app-dir/back-forward-cache/back-forward-cache.test.ts", @@ -268,6 +269,7 @@ "test/e2e/app-dir/static-shell-debugging/static-shell-debugging.test.ts", "test/e2e/app-dir/taint/process-taint.test.ts", "test/e2e/app-dir/temporary-references/temporary-references.test.ts", + "test/e2e/app-dir/typed-routes-validator/typed-routes-validator.test.ts", "test/e2e/app-dir/unauthorized/basic/unauthorized-basic.test.ts", "test/e2e/app-dir/unauthorized/default/unauthorized-default.test.ts", "test/e2e/app-dir/unstable-rethrow/unstable-rethrow.test.ts",