diff --git a/packages/next/src/build/webpack/loaders/next-swc-loader.ts b/packages/next/src/build/webpack/loaders/next-swc-loader.ts index 898de1f4d5..0a83dcb34c 100644 --- a/packages/next/src/build/webpack/loaders/next-swc-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-swc-loader.ts @@ -117,7 +117,9 @@ async function loaderTransform( filename, isServer, isPageFile, - development: this.mode === 'development', + development: + this.mode === 'development' || + !!nextConfig.experimental?.allowDevelopmentBuild, hasReactRefresh, modularizeImports: nextConfig?.modularizeImports, optimizePackageImports: nextConfig?.experimental?.optimizePackageImports, diff --git a/packages/next/src/build/webpack/plugins/define-env-plugin.ts b/packages/next/src/build/webpack/plugins/define-env-plugin.ts index 862e5bc3ea..3726900426 100644 --- a/packages/next/src/build/webpack/plugins/define-env-plugin.ts +++ b/packages/next/src/build/webpack/plugins/define-env-plugin.ts @@ -159,7 +159,10 @@ export function getDefineEnv({ 'process.turbopack': isTurbopack, 'process.env.TURBOPACK': isTurbopack, // TODO: enforce `NODE_ENV` on `process.env`, and add a test: - 'process.env.NODE_ENV': dev ? 'development' : 'production', + 'process.env.NODE_ENV': + dev || config.experimental.allowDevelopmentBuild + ? 'development' + : 'production', 'process.env.NEXT_RUNTIME': isEdgeServer ? 'edge' : isNodeServer diff --git a/packages/next/src/server/config-schema.ts b/packages/next/src/server/config-schema.ts index 26ebee816a..3ce749f863 100644 --- a/packages/next/src/server/config-schema.ts +++ b/packages/next/src/server/config-schema.ts @@ -421,6 +421,7 @@ export const configSchema: zod.ZodType = z.lazy(() => useEarlyImport: z.boolean().optional(), testProxy: z.boolean().optional(), defaultTestRunner: z.enum(SUPPORTED_TEST_RUNNERS_LIST).optional(), + allowDevelopmentBuild: z.literal(true).optional(), }) .optional(), exportPathMap: z diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts index c209be7150..8936702869 100644 --- a/packages/next/src/server/config-shared.ts +++ b/packages/next/src/server/config-shared.ts @@ -452,6 +452,10 @@ export interface ExperimentalConfig { * Set a default test runner to be used by `next experimental-test`. */ defaultTestRunner?: SupportedTestRunners + /** + * Allow NODE_ENV=development even for `next build`. + */ + allowDevelopmentBuild?: true } export type ExportPathMap = { @@ -963,6 +967,7 @@ export const defaultConfig: NextConfig = { dynamic: 30, static: 300, }, + allowDevelopmentBuild: undefined, }, bundlePagesRouterDependencies: false, } diff --git a/packages/next/src/server/config.ts b/packages/next/src/server/config.ts index cbc1d7068c..5abaa77dc1 100644 --- a/packages/next/src/server/config.ts +++ b/packages/next/src/server/config.ts @@ -262,6 +262,15 @@ function assignDefaults( const result = { ...defaultConfig, ...config } + if ( + result.experimental?.allowDevelopmentBuild && + process.env.NODE_ENV !== 'development' + ) { + throw new Error( + `The experimental.allowDevelopmentBuild option requires NODE_ENV to be explicitly set to 'development'.` + ) + } + if ( result.experimental?.ppr && !process.env.__NEXT_VERSION!.includes('canary') && diff --git a/test/production/allow-development-build/allow-development-build.test.ts b/test/production/allow-development-build/allow-development-build.test.ts new file mode 100644 index 0000000000..e9651dd826 --- /dev/null +++ b/test/production/allow-development-build/allow-development-build.test.ts @@ -0,0 +1,74 @@ +import { nextTestSetup } from 'e2e-utils' +import { retry } from 'next-test-utils' + +describe('allow-development-build', () => { + describe('with NODE_ENV set to development', () => { + const { next } = nextTestSetup({ + files: __dirname, + env: { + NODE_ENV: 'development', + }, + nextConfig: { + experimental: { + allowDevelopmentBuild: true, + }, + }, + }) + + it('should warn about a non-standard NODE_ENV', () => { + expect(next.cliOutput).toContain( + 'You are using a non-standard "NODE_ENV" value in your environment' + ) + }) + + it.each(['app-page', 'pages-page'])( + `should show React development errors in %s`, + async (page) => { + const browser = await next.browser(page, { + pushErrorAsConsoleLog: true, + }) + + await retry(async () => { + const logs = await browser.log() + + const errorLogs = logs.filter((log) => log.source === 'error') + + expect(errorLogs).toEqual( + expect.arrayContaining([ + { + message: expect.toBeOneOf([ + expect.stringContaining( + "Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client." + ), + expect.stringContaining( + 'There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.' + ), + ]), + source: 'error', + }, + ]) + ) + }) + } + ) + }) + + describe('with NODE_ENV not set to development', () => { + const { next } = nextTestSetup({ + files: __dirname, + skipStart: true, + nextConfig: { + experimental: { + allowDevelopmentBuild: true, + }, + }, + }) + + it('should fail the build with a message about not setting NODE_ENV', async () => { + await next.start().catch(() => {}) + expect(next.cliOutput).toContain( + "The experimental.allowDevelopmentBuild option requires NODE_ENV to be explicitly set to 'development'" + ) + }) + }) +}) diff --git a/test/production/allow-development-build/app/app-page/page.tsx b/test/production/allow-development-build/app/app-page/page.tsx new file mode 100644 index 0000000000..e4dae6d2c6 --- /dev/null +++ b/test/production/allow-development-build/app/app-page/page.tsx @@ -0,0 +1,11 @@ +'use client' +import React from 'react' + +export default function Page() { + return ( +
+ Hello World{' '} + {typeof window !== 'undefined' && Hydration Error!} +
+ ) +} diff --git a/test/production/allow-development-build/app/layout.tsx b/test/production/allow-development-build/app/layout.tsx new file mode 100644 index 0000000000..dbce4ea8e3 --- /dev/null +++ b/test/production/allow-development-build/app/layout.tsx @@ -0,0 +1,11 @@ +export default function RootLayout({ + children, +}: { + children: React.ReactNode +}) { + return ( + + {children} + + ) +} diff --git a/test/production/allow-development-build/pages/pages-page.tsx b/test/production/allow-development-build/pages/pages-page.tsx new file mode 100644 index 0000000000..d663657c0c --- /dev/null +++ b/test/production/allow-development-build/pages/pages-page.tsx @@ -0,0 +1,8 @@ +export default function Page() { + return ( +
+ Hello World{' '} + {typeof window !== 'undefined' && Hydration Error!} +
+ ) +}