Repository: vercel/next.js
Pull request: Turbopack build: Implement error when using next start without --turbopack

### What?

This PR adds validation to ensure that the build tool used during `next build` matches the one used during `next start`. It prevents mismatches between Turbopack and Webpack builds.

### Why?
When a project is built with Turbopack but started without the `--turbopack` flag (or vice versa), it can lead to unexpected behavior and errors. This change ensures consistency between build and runtime environments.

### How?
- Added a constant `IS_TURBOPACK_BUILD_FILE` to track which build tool was used
- Added validation in the server startup to check if the `--turbopack` flag matches the build tool that was used
- Added appropriate error messages (669 and 670) to provide clear guidance when mismatches occur


Closes PACK-4239

Changed files:
filename: packages/next/errors.json
patch: @@ -666,5 +666,7 @@
   "665": "Failed to find Server Action \"%s\". This request might be from an older or newer deployment.\\nRead more: https://nextjs.org/docs/messages/failed-to-find-server-action",
   "666": "Turbopack builds are only available in canary builds of Next.js.",
   "667": "receiveExpiredTags is deprecated, and not expected to be called.",
-  "668": "Internal Next.js error: Router action dispatched before initialization."
+  "668": "Internal Next.js error: Router action dispatched before initialization.",
+  "669": "Invariant: --turbopack is set but the build used Webpack",
+  "670": "Invariant: --turbopack is not set but the build used Turbopack. Add --turbopack to \"next start\"."
 }

filename: packages/next/src/build/index.ts
patch: @@ -81,6 +81,7 @@ import {
   UNDERSCORE_NOT_FOUND_ROUTE,
   DYNAMIC_CSS_MANIFEST,
   TURBOPACK_CLIENT_MIDDLEWARE_MANIFEST,
+  IS_TURBOPACK_BUILD_FILE,
 } from '../shared/lib/constants'
 import {
   getSortedRoutes,
@@ -1452,7 +1453,7 @@ export default async function build(
       let shutdownPromise = Promise.resolve()
       if (!isGenerateMode) {
         if (turboNextBuild) {
-          await writeFileUtf8(path.join(distDir, 'IS_TURBOPACK_BUILD'), '')
+          await writeFileUtf8(path.join(distDir, IS_TURBOPACK_BUILD_FILE), '')
           const {
             duration: compilerDuration,
             shutdownPromise: p,
@@ -2333,6 +2334,7 @@ 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,6 +42,7 @@ 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'
@@ -188,9 +189,24 @@ export default class NextNodeServer extends BaseServer<
     // Initialize super class
     super(options)
 
-    this.isDev = options.dev ?? false
+    const isDev = options.dev ?? false
+    this.isDev = isDev
     this.sriEnabled = Boolean(options.conf.experimental?.sri?.algorithm)
 
+    const isTurbopackBuild = this.isTurbopackBuild()
+
+    if (!isDev) {
+      if (process.env.TURBOPACK && !isTurbopackBuild) {
+        throw new Error(
+          `Invariant: --turbopack is set but the build used Webpack`
+        )
+      } else if (!process.env.TURBOPACK && isTurbopackBuild) {
+        throw new Error(
+          `Invariant: --turbopack is not set but the build used Turbopack. Add --turbopack to "next start".`
+        )
+      }
+    }
+
     /**
      * This sets environment variable to be used at the time of SSR by head.tsx.
      * Using this from process.env allows targeting SSR by calling
@@ -510,6 +526,11 @@ 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/shared/lib/constants.ts
patch: @@ -56,6 +56,7 @@ 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'