Repository: vercel/next.js
Pull request: Turbopack build: Move Turbopack marker to SERVER_FILES_MANIFEST

### What?

As recommended by @ijjk.

Replaces the `IS_TURBOPACK_BUILD_FILE` marker with a `turbopack` boolean property in the required server files manifest.

Would like a review of this because it does mean we have to ship the require server files manifest into the standalone output which seemingly did not happen before. If we'd want to avoid that the current approach of writing a small file is better.


### How?

- Added a `turbopack` boolean property to the `RequiredServerFilesManifest` interface
- Removed the `IS_TURBOPACK_BUILD_FILE` constant and related file operations
- Updated the server to check the manifest property instead of looking for the marker file
- Updated tests to verify the manifest property instead of the marker file


Closes PACK-4255

Changed files:
filename: packages/next/src/build/index.ts
patch: @@ -81,7 +81,6 @@ import {
   UNDERSCORE_NOT_FOUND_ROUTE,
   DYNAMIC_CSS_MANIFEST,
   TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST,
-  IS_TURBOPACK_BUILD_FILE,
 } from '../shared/lib/constants'
 import {
   getSortedRoutes,
@@ -1453,7 +1452,6 @@ export default async function build(
       let shutdownPromise = Promise.resolve()
       if (!isGenerateMode) {
         if (turboNextBuild) {
-          await writeFileUtf8(path.join(distDir, IS_TURBOPACK_BUILD_FILE), '')
           const {
             duration: compilerDuration,
             shutdownPromise: p,
@@ -2279,6 +2277,7 @@ export default async function build(
 
                 // @ts-expect-error internal field TODO: fix this, should use a separate mechanism to pass the info.
                 isExperimentalCompile: isCompileMode,
+                isTurbopackBuild: turboNextBuild,
               },
             },
             appDir: dir,
@@ -2334,7 +2333,6 @@ export default async function build(
                   ]
                 : []),
               BUILD_ID_FILE,
-              turboNextBuild ? IS_TURBOPACK_BUILD_FILE : null,
               path.join(SERVER_DIRECTORY, NEXT_FONT_MANIFEST + '.js'),
               path.join(SERVER_DIRECTORY, NEXT_FONT_MANIFEST + '.json'),
               ...instrumentationHookEntryFiles,

filename: packages/next/src/server/next-server.ts
patch: @@ -42,7 +42,6 @@ import {
   PHASE_PRODUCTION_BUILD,
   UNDERSCORE_NOT_FOUND_ROUTE_ENTRY,
   FUNCTIONS_CONFIG_MANIFEST,
-  IS_TURBOPACK_BUILD_FILE,
 } from '../shared/lib/constants'
 import { findDir } from '../lib/find-pages-dir'
 import { NodeNextRequest, NodeNextResponse } from './base-http/node'
@@ -193,9 +192,9 @@ export default class NextNodeServer extends BaseServer<
     this.isDev = isDev
     this.sriEnabled = Boolean(options.conf.experimental?.sri?.algorithm)
 
-    const isTurbopackBuild = this.isTurbopackBuild()
-
-    if (!isDev) {
+    // @ts-expect-error internal field not publicly exposed
+    const isTurbopackBuild = this.nextConfig.experimental?.isTurbopackBuild
+    if (!isDev && typeof isTurbopackBuild !== 'undefined') {
       if (process.env.TURBOPACK && !isTurbopackBuild) {
         throw new Error(
           `Invariant: --turbopack is set but the build used Webpack`
@@ -526,11 +525,6 @@ export default class NextNodeServer extends BaseServer<
     }
   }
 
-  private isTurbopackBuild(): boolean {
-    const isTurbopackBuildFile = join(this.distDir, IS_TURBOPACK_BUILD_FILE)
-    return fs.existsSync(isTurbopackBuildFile)
-  }
-
   protected getEnabledDirectories(dev: boolean): NextEnabledDirectories {
     const dir = dev ? this.dir : this.serverDistDir
 

filename: packages/next/src/server/next.ts
patch: @@ -231,15 +231,18 @@ export class NextServer implements NextWrapperServer {
     )
 
     // check serialized build config when available
-    if (process.env.NODE_ENV === 'production') {
+    if (!this.options.dev) {
       try {
         const serializedConfig = require(
-          path.join(dir, '.next', SERVER_FILES_MANIFEST)
+          path.join(dir, config.distDir, SERVER_FILES_MANIFEST)
         ).config
 
         // @ts-expect-error internal field
         config.experimental.isExperimentalCompile =
           serializedConfig.experimental.isExperimentalCompile
+        // @ts-expect-error internal field
+        config.experimental.isTurbopackBuild =
+          serializedConfig.experimental.isTurbopackBuild
       } catch (_) {
         // if distDir is customized we don't know until we
         // load the config so fallback to loading the config

filename: packages/next/src/shared/lib/constants.ts
patch: @@ -56,7 +56,6 @@ export const CONFIG_FILES = [
   'next.config.ts',
 ]
 export const BUILD_ID_FILE = 'BUILD_ID'
-export const IS_TURBOPACK_BUILD_FILE = 'IS_TURBOPACK_BUILD'
 export const BLOCKED_PAGES = ['/_document', '/_app', '/_error']
 export const CLIENT_PUBLIC_FILES_PATH = 'public'
 export const CLIENT_STATIC_FILES_PATH = 'static'

filename: test/production/app-dir/turbopack-build-marker/turbopack-build-marker.test.ts
patch: @@ -5,8 +5,11 @@ describe('turbopack-build-marker', () => {
   })
 
   it('should have Turbopack build marker', async () => {
-    expect(await next.hasFile('.next/IS_TURBOPACK_BUILD')).toBe(
-      !!process.env.TURBOPACK
+    const requiredServerFilesManifest = await next.readJSON(
+      '.next/required-server-files.json'
     )
+    expect(
+      requiredServerFilesManifest.config.experimental.isTurbopackBuild
+    ).toBe(!!process.env.TURBOPACK)
   })
 })