From f21c95d4300c714d2a3768a39de05054cd7bdd09 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 11 Oct 2024 17:58:01 +0200 Subject: [PATCH 1/5] Test `'use cache'` in a route handler --- test/e2e/app-dir/use-cache/app/api/route.ts | 17 +++++++++++++++++ test/e2e/app-dir/use-cache/use-cache.test.ts | 7 +++++++ 2 files changed, 24 insertions(+) create mode 100644 test/e2e/app-dir/use-cache/app/api/route.ts diff --git a/test/e2e/app-dir/use-cache/app/api/route.ts b/test/e2e/app-dir/use-cache/app/api/route.ts new file mode 100644 index 0000000000000..d52bf97fa2c8d --- /dev/null +++ b/test/e2e/app-dir/use-cache/app/api/route.ts @@ -0,0 +1,17 @@ +async function getCachedRandom() { + 'use cache' + return Math.random() +} + +export async function GET() { + const rand1 = await getCachedRandom() + // TODO: Remove this extra micro task when bug in use cache wrapper is fixed. + await Promise.resolve() + const rand2 = await getCachedRandom() + + const response = JSON.stringify({ rand1, rand2 }) + + return new Response(response, { + headers: { 'content-type': 'application/json' }, + }) +} diff --git a/test/e2e/app-dir/use-cache/use-cache.test.ts b/test/e2e/app-dir/use-cache/use-cache.test.ts index 5d018664d0359..2cb293e0b4588 100644 --- a/test/e2e/app-dir/use-cache/use-cache.test.ts +++ b/test/e2e/app-dir/use-cache/use-cache.test.ts @@ -74,4 +74,11 @@ describe('use-cache', () => { ) } }) + + itSkipTurbopack('should cache results in route handlers', async () => { + const response = await next.fetch('/api') + const { rand1, rand2 } = await response.json() + + expect(rand1).toEqual(rand2) + }) }) From 530aadb08f5956520ed1ff8fe8c99fb3ec5a4c13 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 11 Oct 2024 18:13:58 +0200 Subject: [PATCH 2/5] Generate server and client reference manifests for route handlers This is required for `'use cache'` to work in route handlers. --- .../src/build/flying-shuttle/stitch-builds.ts | 2 +- .../webpack/plugins/flight-manifest-plugin.ts | 6 +++++ .../plugins/next-trace-entrypoints-plugin.ts | 25 ++++++++----------- packages/next/src/build/webpack/utils.ts | 7 +----- packages/next/src/server/load-components.ts | 7 +----- 5 files changed, 19 insertions(+), 28 deletions(-) diff --git a/packages/next/src/build/flying-shuttle/stitch-builds.ts b/packages/next/src/build/flying-shuttle/stitch-builds.ts index c1f784b0b70fc..3329ed9466bd2 100644 --- a/packages/next/src/build/flying-shuttle/stitch-builds.ts +++ b/packages/next/src/build/flying-shuttle/stitch-builds.ts @@ -106,7 +106,7 @@ export async function stitchBuilds( path.join(distDir, entryFile + '.nft.json') ) - if (type === 'app' && !entry.endsWith('/route')) { + if (type === 'app') { const clientRefManifestFile = path.join( 'server', type, diff --git a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts index 58f859be36a25..bcafc9153a230 100644 --- a/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/src/build/webpack/plugins/flight-manifest-plugin.ts @@ -519,6 +519,12 @@ export class ClientReferenceManifestPlugin { manifestEntryFiles.push(entryName.replace(/\/page(\.[^/]+)?$/, '/page')) } + // We also need to create manifests for route handler entrypoints to + // enable `'use cache'`. + if (/\/route$/.test(entryName)) { + manifestEntryFiles.push(entryName) + } + const groupName = entryNameToGroupName(entryName) if (!manifestsPerGroup.has(groupName)) { manifestsPerGroup.set(groupName, []) diff --git a/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts b/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts index c0bf1675fdb6a..dc02489789d45 100644 --- a/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts +++ b/packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts @@ -9,7 +9,6 @@ import type { NodeFileTraceReasons } from 'next/dist/compiled/@vercel/nft' import { CLIENT_REFERENCE_MANIFEST, TRACE_OUTPUT_VERSION, - UNDERSCORE_NOT_FOUND_ROUTE_ENTRY, type CompilerNameValues, } from '../../../shared/lib/constants' import { webpack, sources } from 'next/dist/compiled/webpack/webpack' @@ -279,21 +278,17 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance { if (entrypoint.name.startsWith('app/')) { // include the client reference manifest - const clientManifestsForPage = - entrypoint.name.endsWith('/page') || - entrypoint.name === UNDERSCORE_NOT_FOUND_ROUTE_ENTRY - ? nodePath.join( - outputPath, - outputPrefix, - entrypoint.name.replace(/%5F/g, '_') + - '_' + - CLIENT_REFERENCE_MANIFEST + - '.js' - ) - : null + const clientManifestsForEntrypoint = nodePath.join( + outputPath, + outputPrefix, + entrypoint.name.replace(/%5F/g, '_') + + '_' + + CLIENT_REFERENCE_MANIFEST + + '.js' + ) - if (clientManifestsForPage !== null) { - entryFiles.add(clientManifestsForPage) + if (clientManifestsForEntrypoint !== null) { + entryFiles.add(clientManifestsForEntrypoint) } } diff --git a/packages/next/src/build/webpack/utils.ts b/packages/next/src/build/webpack/utils.ts index 307aedef19ba5..01afb14e48380 100644 --- a/packages/next/src/build/webpack/utils.ts +++ b/packages/next/src/build/webpack/utils.ts @@ -6,7 +6,6 @@ import type { Module, ModuleGraph, } from 'webpack' -import { isAppRouteRoute } from '../../lib/is-app-route-route' import type { ModuleGraphConnection } from 'webpack' export function traverseModules( @@ -48,11 +47,7 @@ export function forEachEntryModule( ) { for (const [name, entry] of compilation.entries.entries()) { // Skip for entries under pages/ - if ( - name.startsWith('pages/') || - // Skip for route.js entries - (name.startsWith('app/') && isAppRouteRoute(name)) - ) { + if (name.startsWith('pages/')) { continue } diff --git a/packages/next/src/server/load-components.ts b/packages/next/src/server/load-components.ts index 6cdc69b5c7021..388ac4f1ec80c 100644 --- a/packages/next/src/server/load-components.ts +++ b/packages/next/src/server/load-components.ts @@ -19,7 +19,6 @@ import { REACT_LOADABLE_MANIFEST, CLIENT_REFERENCE_MANIFEST, SERVER_REFERENCE_MANIFEST, - UNDERSCORE_NOT_FOUND_ROUTE, } from '../shared/lib/constants' import { join } from 'path' import { requirePage } from './require' @@ -140,10 +139,6 @@ async function loadComponentsImpl({ ]) } - // Make sure to avoid loading the manifest for Route Handlers - const hasClientManifest = - isAppPath && (page.endsWith('/page') || page === UNDERSCORE_NOT_FOUND_ROUTE) - // Load the manifest files first const [ buildManifest, @@ -155,7 +150,7 @@ async function loadComponentsImpl({ loadManifestWithRetries( join(distDir, REACT_LOADABLE_MANIFEST) ), - hasClientManifest + isAppPath ? loadClientReferenceManifest( join( distDir, From b68ded47c27658b566e9e33d9014b54fa212d822 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 11 Oct 2024 18:16:44 +0200 Subject: [PATCH 3/5] Re-enable `Math.random()` test with `'use cache'` in route handler --- test/e2e/app-dir/node-extensions/node-extensions.random.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/app-dir/node-extensions/node-extensions.random.test.ts b/test/e2e/app-dir/node-extensions/node-extensions.random.test.ts index c2b9d2fbf203b..eeb9f42929cf4 100644 --- a/test/e2e/app-dir/node-extensions/node-extensions.random.test.ts +++ b/test/e2e/app-dir/node-extensions/node-extensions.random.test.ts @@ -65,7 +65,7 @@ describe('Node Extensions', () => { expect($('li').length).toBe(2) }) - it.skip('should not error when accessing routes that use Math.random() in App Router', async () => { + it('should not error when accessing routes that use Math.random() in App Router', async () => { let res, body res = await next.fetch('/app/prerendered/uncached/api') From e483eff49fc3ee8367358e1870bcda7741ee3ae8 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Fri, 11 Oct 2024 18:53:22 +0200 Subject: [PATCH 4/5] Update `.next` files snapshot to include route client manifests --- test/e2e/app-dir/app-static/app-static.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 22fefa68ac985..6aa87fbedf8f5 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -754,6 +754,7 @@ describe('app-dir static/dynamic handling', () => { "(new)/custom/page.js", "(new)/custom/page_client-reference-manifest.js", "(new)/default-config-fetch/api/route.js", + "(new)/default-config-fetch/api/route_client-reference-manifest.js", "(new)/default-config-fetch/page.js", "(new)/default-config-fetch/page_client-reference-manifest.js", "(new)/no-config-fetch/page.js", @@ -763,11 +764,15 @@ describe('app-dir static/dynamic handling', () => { "_not-found/page.js", "_not-found/page_client-reference-manifest.js", "api/draft-mode/route.js", + "api/draft-mode/route_client-reference-manifest.js", "api/large-data/route.js", + "api/large-data/route_client-reference-manifest.js", "api/revalidate-path-edge/route.js", "api/revalidate-path-node/route.js", + "api/revalidate-path-node/route_client-reference-manifest.js", "api/revalidate-tag-edge/route.js", "api/revalidate-tag-node/route.js", + "api/revalidate-tag-node/route_client-reference-manifest.js", "articles/[slug]/page.js", "articles/[slug]/page_client-reference-manifest.js", "articles/works.html", @@ -819,15 +824,19 @@ describe('app-dir static/dynamic handling', () => { "force-dynamic-fetch-cache/default-cache/page.js", "force-dynamic-fetch-cache/default-cache/page_client-reference-manifest.js", "force-dynamic-fetch-cache/default-cache/route/route.js", + "force-dynamic-fetch-cache/default-cache/route/route_client-reference-manifest.js", "force-dynamic-fetch-cache/force-cache/page.js", "force-dynamic-fetch-cache/force-cache/page_client-reference-manifest.js", "force-dynamic-fetch-cache/force-cache/route/route.js", + "force-dynamic-fetch-cache/force-cache/route/route_client-reference-manifest.js", "force-dynamic-fetch-cache/no-fetch-cache/page.js", "force-dynamic-fetch-cache/no-fetch-cache/page_client-reference-manifest.js", "force-dynamic-fetch-cache/no-fetch-cache/route/route.js", + "force-dynamic-fetch-cache/no-fetch-cache/route/route_client-reference-manifest.js", "force-dynamic-fetch-cache/with-fetch-cache/page.js", "force-dynamic-fetch-cache/with-fetch-cache/page_client-reference-manifest.js", "force-dynamic-fetch-cache/with-fetch-cache/route/route.js", + "force-dynamic-fetch-cache/with-fetch-cache/route/route_client-reference-manifest.js", "force-dynamic-no-prerender/[id]/page.js", "force-dynamic-no-prerender/[id]/page_client-reference-manifest.js", "force-dynamic-prerender/[slug]/page.js", @@ -920,11 +929,17 @@ describe('app-dir static/dynamic handling', () => { "response-url/page_client-reference-manifest.js", "route-handler-edge/revalidate-360/route.js", "route-handler/no-store-force-static/route.js", + "route-handler/no-store-force-static/route_client-reference-manifest.js", "route-handler/no-store/route.js", + "route-handler/no-store/route_client-reference-manifest.js", "route-handler/post/route.js", + "route-handler/post/route_client-reference-manifest.js", "route-handler/revalidate-360-isr/route.js", + "route-handler/revalidate-360-isr/route_client-reference-manifest.js", "route-handler/revalidate-360/route.js", + "route-handler/revalidate-360/route_client-reference-manifest.js", "route-handler/static-cookies/route.js", + "route-handler/static-cookies/route_client-reference-manifest.js", "specify-new-tags/one-tag/page.js", "specify-new-tags/one-tag/page_client-reference-manifest.js", "specify-new-tags/two-tags/page.js", @@ -949,6 +964,7 @@ describe('app-dir static/dynamic handling', () => { "stale-cache-serving/app-page/page.js", "stale-cache-serving/app-page/page_client-reference-manifest.js", "stale-cache-serving/route-handler/route.js", + "stale-cache-serving/route-handler/route_client-reference-manifest.js", "static-to-dynamic-error-forced/[id]/page.js", "static-to-dynamic-error-forced/[id]/page_client-reference-manifest.js", "static-to-dynamic-error/[id]/page.js", @@ -996,7 +1012,9 @@ describe('app-dir static/dynamic handling', () => { "variable-revalidate/authorization/page.js", "variable-revalidate/authorization/page_client-reference-manifest.js", "variable-revalidate/authorization/route-cookies/route.js", + "variable-revalidate/authorization/route-cookies/route_client-reference-manifest.js", "variable-revalidate/authorization/route-request/route.js", + "variable-revalidate/authorization/route-request/route_client-reference-manifest.js", "variable-revalidate/cookie.html", "variable-revalidate/cookie.rsc", "variable-revalidate/cookie/page.js", From 80ee082811f1c7829a1ed854d24d3f2d30b0fafb Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Sun, 13 Oct 2024 00:40:01 +0200 Subject: [PATCH 5/5] Disable flaky app-static test for now --- .../e2e/app-dir/app-static/app-static.test.ts | 63 ++++++++++--------- 1 file changed, 35 insertions(+), 28 deletions(-) diff --git a/test/e2e/app-dir/app-static/app-static.test.ts b/test/e2e/app-dir/app-static/app-static.test.ts index 6aa87fbedf8f5..2f3e884c72a05 100644 --- a/test/e2e/app-dir/app-static/app-static.test.ts +++ b/test/e2e/app-dir/app-static/app-static.test.ts @@ -15,17 +15,18 @@ import stripAnsi from 'strip-ansi' const glob = promisify(globOrig) describe('app-dir static/dynamic handling', () => { - const { next, isNextDev, isNextStart, isNextDeploy } = nextTestSetup({ - files: __dirname, - env: { - NEXT_DEBUG_BUILD: '1', - ...(process.env.CUSTOM_CACHE_HANDLER - ? { - CUSTOM_CACHE_HANDLER: process.env.CUSTOM_CACHE_HANDLER, - } - : {}), - }, - }) + const { next, isNextDev, isNextStart, isNextDeploy, isTurbopack } = + nextTestSetup({ + files: __dirname, + env: { + NEXT_DEBUG_BUILD: '1', + ...(process.env.CUSTOM_CACHE_HANDLER + ? { + CUSTOM_CACHE_HANDLER: process.env.CUSTOM_CACHE_HANDLER, + } + : {}), + }, + }) let prerenderManifest let buildCliOutputIndex = 0 @@ -447,30 +448,36 @@ describe('app-dir static/dynamic handling', () => { }) if (!isNextDev && !process.env.CUSTOM_CACHE_HANDLER) { - it('should properly revalidate a route handler that triggers dynamic usage with force-static', async () => { - // wait for the revalidation period - let res = await next.fetch('/route-handler/no-store-force-static') + // TODO: Temporarily disabling this test for Turbopack. The test is failing + // quite often (see https://app.datadoghq.com/ci/test-runs?query=test_level%3Atest%20env%3Aci%20%40git.repository.id%3Agithub.com%2Fvercel%2Fnext.js%20%40test.service%3Anextjs%20%40test.status%3Afail%20%40test.name%3A%22app-dir%20static%2Fdynamic%20handling%20should%20properly%20revalidate%20a%20route%20handler%20that%20triggers%20dynamic%20usage%20with%20force-static%22&agg_m=count&agg_m_source=base&agg_t=count¤tTab=overview&eventStack=&fromUser=false&index=citest&start=1720993078523&end=1728769078523&paused=false). + // Since this is also reproducible when manually recreating the scenario, it + // might actually be a bug with ISR, which needs to be investigated. + if (!isTurbopack) { + it('should properly revalidate a route handler that triggers dynamic usage with force-static', async () => { + // wait for the revalidation period + let res = await next.fetch('/route-handler/no-store-force-static') - let data = await res.json() - // grab the initial timestamp - const initialTimestamp = data.now + let data = await res.json() + // grab the initial timestamp + const initialTimestamp = data.now - // confirm its cached still - res = await next.fetch('/route-handler/no-store-force-static') + // confirm its cached still + res = await next.fetch('/route-handler/no-store-force-static') - data = await res.json() + data = await res.json() - expect(data.now).toBe(initialTimestamp) + expect(data.now).toBe(initialTimestamp) - // wait for the revalidation time - await waitFor(3000) + // wait for the revalidation time + await waitFor(3000) - // verify fresh data - res = await next.fetch('/route-handler/no-store-force-static') - data = await res.json() + // verify fresh data + res = await next.fetch('/route-handler/no-store-force-static') + data = await res.json() - expect(data.now).not.toBe(initialTimestamp) - }) + expect(data.now).not.toBe(initialTimestamp) + }) + } } if (!process.env.CUSTOM_CACHE_HANDLER) {