Repository: vercel/next.js
Pull request: Turbopack: pass cache handler path as relative path to Rust

Preparation for using this as an entry for NFT tracing from the Rust side. This simplifies the path handling on the Rust side.
We do the same for the `images.loaderFile` already.

Closes PACK-5287

Changed files:
filename: crates/next-core/src/next_config.rs
patch: @@ -29,6 +29,7 @@ use turbopack_ecmascript_plugins::transform::{
 use turbopack_node::transforms::webpack::{WebpackLoaderItem, WebpackLoaderItems};
 
 use crate::{
+    app_structure::FileSystemPathVec,
     mode::NextMode,
     next_import_map::mdx_import_source_file,
     next_shared::{
@@ -1127,6 +1128,9 @@ pub struct OptionalMdxTransformOptions(Option<ResolvedVc<MdxTransformOptions>>);
 
 pub struct OptionSubResourceIntegrity(Option<SubResourceIntegrity>);
 
+#[turbo_tasks::value(transparent)]
+pub struct OptionFileSystemPath(Option<FileSystemPath>);
+
 #[turbo_tasks::value(transparent)]
 pub struct OptionServerActions(Option<ServerActions>);
 
@@ -1226,8 +1230,12 @@ impl NextConfig {
     }
 
     #[turbo_tasks::function]
-    pub fn cache_handler(&self) -> Vc<Option<RcStr>> {
-        Vc::cell(self.cache_handler.clone())
+    pub fn cache_handler(&self, project_path: FileSystemPath) -> Result<Vc<OptionFileSystemPath>> {
+        if let Some(handler) = &self.cache_handler {
+            Ok(Vc::cell(Some(project_path.join(handler)?)))
+        } else {
+            Ok(Vc::cell(None))
+        }
     }
 
     #[turbo_tasks::function]
@@ -1359,8 +1367,7 @@ impl NextConfig {
                         // emit an issue to prevent users from encountering duplicate module names.
                         if ext.contains("*") && rename_as.as_ref().is_some_and(|r| !r.contains("*"))
                         {
-                            let config_file_path =
-                                project_path.join(&format!("./{}", self.config_file_name))?;
+                            let config_file_path = project_path.join(&self.config_file_name)?;
 
                             InvalidLoaderRuleError {
                                 ext: ext.clone(),
@@ -1481,6 +1488,23 @@ impl NextConfig {
         Vc::cell(self.modularize_imports.clone().unwrap_or_default())
     }
 
+    #[turbo_tasks::function]
+    pub fn experimental_cache_handlers(
+        &self,
+        project_path: FileSystemPath,
+    ) -> Result<Vc<FileSystemPathVec>> {
+        if let Some(handlers) = &self.experimental.cache_handlers {
+            Ok(Vc::cell(
+                handlers
+                    .values()
+                    .map(|h| project_path.join(h))
+                    .collect::<Result<Vec<_>>>()?,
+            ))
+        } else {
+            Ok(Vc::cell(vec![]))
+        }
+    }
+
     #[turbo_tasks::function]
     pub fn experimental_swc_plugins(&self) -> Vc<SwcPlugins> {
         Vc::cell(self.experimental.swc_plugins.clone().unwrap_or_default())

filename: packages/next/src/build/swc/index.ts
patch: @@ -873,45 +873,72 @@ function bindingToApi(
     let nextConfigSerializable = augmentNextConfig(nextConfig, projectPath)
 
     nextConfigSerializable.generateBuildId =
-      await nextConfig.generateBuildId?.()
+      await nextConfigSerializable.generateBuildId?.()
 
     // TODO: these functions takes arguments, have to be supported in a different way
     nextConfigSerializable.exportPathMap = {}
-    nextConfigSerializable.webpack = nextConfig.webpack && {}
+    nextConfigSerializable.webpack = nextConfigSerializable.webpack && {}
 
     if (nextConfigSerializable.turbopack?.rules) {
       ensureLoadersHaveSerializableOptions(
         nextConfigSerializable.turbopack?.rules
       )
     }
 
-    nextConfigSerializable.modularizeImports =
-      nextConfigSerializable.modularizeImports
-        ? Object.fromEntries(
-            Object.entries<any>(nextConfigSerializable.modularizeImports).map(
-              ([mod, config]) => [
-                mod,
-                {
-                  ...config,
-                  transform:
-                    typeof config.transform === 'string'
-                      ? config.transform
-                      : Object.entries(config.transform).map(([key, value]) => [
-                          key,
-                          value,
-                        ]),
-                },
-              ]
-            )
-          )
-        : undefined
+    if (nextConfigSerializable.modularizeImports) {
+      nextConfigSerializable.modularizeImports = Object.fromEntries(
+        Object.entries<any>(nextConfigSerializable.modularizeImports).map(
+          ([mod, config]) => [
+            mod,
+            {
+              ...config,
+              transform:
+                typeof config.transform === 'string'
+                  ? config.transform
+                  : Object.entries(config.transform),
+            },
+          ]
+        )
+      )
+    }
 
     // loaderFile is an absolute path, we need it to be relative for turbopack.
     if (nextConfigSerializable.images.loaderFile) {
       nextConfigSerializable.images = {
-        ...nextConfig.images,
+        ...nextConfigSerializable.images,
         loaderFile:
-          './' + path.relative(projectPath, nextConfig.images.loaderFile),
+          './' +
+          path.relative(projectPath, nextConfigSerializable.images.loaderFile),
+      }
+    }
+
+    // cacheHandler can be an absolute path, we need it to be relative for turbopack.
+    if (nextConfigSerializable.cacheHandler) {
+      nextConfigSerializable.cacheHandler =
+        './' +
+        (path.isAbsolute(nextConfigSerializable.cacheHandler)
+          ? path.relative(projectPath, nextConfigSerializable.cacheHandler)
+          : nextConfigSerializable.cacheHandler)
+    }
+    if (nextConfigSerializable.experimental?.cacheHandlers) {
+      nextConfigSerializable.experimental = {
+        ...nextConfigSerializable.experimental,
+        cacheHandlers: Object.fromEntries(
+          Object.entries(
+            nextConfigSerializable.experimental.cacheHandlers as Record<
+              string,
+              string
+            >
+          )
+            .filter(([_, value]) => value != null)
+            .map(([key, value]) => [
+              key,
+              './' +
+                (path.isAbsolute(value)
+                  ? path.relative(projectPath, value)
+                  : value),
+            ])
+        ),
       }
     }
 

filename: test/e2e/app-dir/app-static/app-static-custom-handler.test.ts
patch: @@ -1,2 +1,2 @@
-process.env.CUSTOM_CACHE_HANDLER = require.resolve('./cache-handler.js')
+process.env.CUSTOM_CACHE_HANDLER = '1'
 require('./app-static.test')

filename: test/e2e/app-dir/app-static/next.config.js
patch: @@ -3,7 +3,9 @@ module.exports = {
   logging: {
     fetches: {},
   },
-  cacheHandler: process.env.CUSTOM_CACHE_HANDLER,
+  cacheHandler: process.env.CUSTOM_CACHE_HANDLER
+    ? require.resolve('./cache-handler.js')
+    : undefined,
 
   rewrites: async () => {
     return {

filename: test/e2e/app-dir/use-cache-unknown-cache-kind/use-cache-unknown-cache-kind.test.ts
patch: @@ -1,5 +1,4 @@
 import { nextTestSetup } from 'e2e-utils'
-import { NextConfig } from 'next'
 import {
   assertHasRedbox,
   assertNoRedbox,
@@ -12,17 +11,6 @@ import { createSandbox } from 'development-sandbox'
 
 const isRspack = !!process.env.NEXT_RSPACK
 
-const nextConfigWithCacheHandler: NextConfig = {
-  experimental: {
-    cacheComponents: true,
-    cacheHandlers: {
-      custom: require.resolve(
-        'next/dist/server/lib/cache-handlers/default.external'
-      ),
-    },
-  },
-}
-
 describe('use-cache-unknown-cache-kind', () => {
   const { next, isNextStart, isTurbopack, skipped } = nextTestSetup({
     files: __dirname,
@@ -191,7 +179,16 @@ describe('use-cache-unknown-cache-kind', () => {
 
       await session.patch(
         'next.config.js',
-        `module.exports = ${JSON.stringify(nextConfigWithCacheHandler)}`
+        `module.exports = {
+          experimental: {
+            cacheComponents: true,
+            cacheHandlers: {
+              custom: require.resolve(
+                'next/dist/server/lib/cache-handlers/default.external'
+              ),
+            },
+          },
+        }`
       )
 
       await retry(async () => {