Repository: vercel/next.js
Pull request: Turbopack: canary-gate production builds

This allows `next build --turbopack` to be run without `TURBOPACK_BUILD` being set, and only allows it when used with canary Next.js builds.

This also accepts `next build --turbo` and `next build --experimental-turbo` for compatibility.

Note: This keeps `TURBOPACK_BUILD` for test purposes.

Closes PACK-4108
Closes PACK-4111

Test Plan:

- `pnpm next build --turbopack examples/basic-css/` uses Turbopack to build and succeeds
- Set `next`’s package.json version to `15.3.0`, build, and run `./node_modules/.bin/next build --turbopack examples/basic-css/`. Verify the canary-gate message is shown


Changed files:
filename: packages/next/errors.json
patch: @@ -663,5 +663,6 @@
   "662": "Invariant failed to find webpack runtime chunk",
   "663": "Invariant: client chunk changed but failed to detect hash %s",
   "664": "Missing 'next-action' header.",
-  "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"
+  "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."
 }

filename: packages/next/src/bin/next.ts
patch: @@ -130,6 +130,8 @@ program
   .option('--profile', 'Enables production profiling for React.')
   .option('--experimental-app-only', 'Builds only App Router routes.')
   .addOption(new Option('--experimental-turbo').hideHelp())
+  .addOption(new Option('--turbo').hideHelp())
+  .addOption(new Option('--turbopack').hideHelp())
   .addOption(
     new Option(
       '--experimental-build-mode [mode]',

filename: packages/next/src/build/turbopack-build/impl.ts
patch: @@ -19,16 +19,17 @@ import loadConfig from '../../server/config'
 import { hasCustomExportOutput } from '../../export/utils'
 import { Telemetry } from '../../telemetry/storage'
 import { setGlobal } from '../../trace'
-
-const IS_TURBOPACK_BUILD = process.env.TURBOPACK && process.env.TURBOPACK_BUILD
+import { isStableBuild, CanaryOnlyError } from '../../shared/lib/canary-only'
 
 export async function turbopackBuild(): Promise<{
   duration: number
   buildTraceContext: undefined
   shutdownPromise: Promise<void>
 }> {
-  if (!IS_TURBOPACK_BUILD) {
-    throw new Error("next build doesn't support turbopack yet")
+  if (isStableBuild()) {
+    throw new CanaryOnlyError(
+      'Turbopack builds are only available in canary builds of Next.js.'
+    )
   }
 
   await validateTurboNextConfig({

filename: packages/next/src/cli/next-build.ts
patch: @@ -16,6 +16,8 @@ export type NextBuildOptions = {
   profile?: boolean
   lint: boolean
   mangling: boolean
+  turbo?: boolean
+  turbopack?: boolean
   experimentalDebugMemoryUsage: boolean
   experimentalAppOnly?: boolean
   experimentalTurbo?: boolean
@@ -34,11 +36,13 @@ const nextBuild = (options: NextBuildOptions, directory?: string) => {
     lint,
     mangling,
     experimentalAppOnly,
-    experimentalTurbo,
     experimentalBuildMode,
     experimentalUploadTrace,
   } = options
 
+  const useTurbopack =
+    options.experimentalTurbo || options.turbo || options.turbopack
+
   let traceUploadUrl: string | undefined
   if (experimentalUploadTrace && !process.env.NEXT_TRACE_UPLOAD_DISABLED) {
     traceUploadUrl = experimentalUploadTrace
@@ -71,7 +75,7 @@ const nextBuild = (options: NextBuildOptions, directory?: string) => {
     printAndExit(`> No such directory exists as the project root: ${dir}`)
   }
 
-  if (experimentalTurbo) {
+  if (useTurbopack) {
     process.env.TURBOPACK = '1'
   }