diff --git a/packages/next/build/compiler.ts b/packages/next/build/compiler.ts index 074211a97dbf6..41c7202ac9bff 100644 --- a/packages/next/build/compiler.ts +++ b/packages/next/build/compiler.ts @@ -5,6 +5,7 @@ import { Span } from '../trace' export type CompilerResult = { errors: webpack5.StatsError[] warnings: webpack5.StatsError[] + stats: webpack5.Stats | undefined } function generateStats( @@ -54,6 +55,7 @@ export function runCompiler( return resolve({ errors: [{ message: reason, details: (err as any).details }], warnings: [], + stats, }) } return reject(err) @@ -61,7 +63,9 @@ export function runCompiler( const result = webpackCloseSpan .traceChild('webpack-generate-error-stats') - .traceFn(() => generateStats({ errors: [], warnings: [] }, stats)) + .traceFn(() => + generateStats({ errors: [], warnings: [], stats }, stats) + ) return resolve(result) }) }) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index bf12b637200f0..59576d4f0ca4b 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -76,7 +76,7 @@ import { eventPackageUsedInGetServerSideProps, } from '../telemetry/events' import { Telemetry } from '../telemetry/storage' -import { CompilerResult, runCompiler } from './compiler' +import { runCompiler } from './compiler' import { createEntrypoints, createPagesMapping, @@ -95,11 +95,13 @@ import { PageInfo, printCustomRoutes, printTreeView, + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage, getUnresolvedModuleFromError, copyTracedFiles, isReservedPage, isCustomErrorPage, isFlightPage, + isEdgeRuntimeCompiled, } from './utils' import getBaseWebpackConfig from './webpack-config' import { PagesManifest } from './webpack/plugins/pages-manifest-plugin' @@ -134,6 +136,16 @@ export type PrerenderManifest = { preview: __ApiPreviewProps } +type CompilerResult = { + errors: webpack.StatsError[] + warnings: webpack.StatsError[] + stats: [ + webpack.Stats | undefined, + webpack.Stats | undefined, + webpack.Stats | undefined + ] +} + export default async function build( dir: string, conf = null, @@ -162,7 +174,6 @@ export default async function build( // We enable concurrent features (Fizz-related rendering architecture) when // using React 18 or experimental. const hasReactRoot = shouldUseReactRoot() - const hasConcurrentFeatures = hasReactRoot const hasServerComponents = hasReactRoot && !!config.experimental.serverComponents @@ -608,7 +619,11 @@ export default async function build( ignore: [] as string[], })) - let result: CompilerResult = { warnings: [], errors: [] } + let result: CompilerResult = { + warnings: [], + errors: [], + stats: [undefined, undefined, undefined], + } let webpackBuildStart let telemetryPlugin await (async () => { @@ -683,6 +698,7 @@ export default async function build( result = { warnings: [...clientResult.warnings], errors: [...clientResult.errors], + stats: [clientResult.stats, undefined, undefined], } } else { const serverResult = await runCompiler(configs[1], { runWebpackSpan }) @@ -701,6 +717,11 @@ export default async function build( ...serverResult.errors, ...(edgeServerResult?.errors || []), ], + stats: [ + clientResult.stats, + serverResult.stats, + edgeServerResult?.stats, + ], } } }) @@ -749,11 +770,21 @@ export default async function build( console.error(error) console.error() - // When using the web runtime, common Node.js native APIs are not available. - const moduleName = getUnresolvedModuleFromError(error) - if (hasConcurrentFeatures && moduleName) { + const clientCompilation = result.stats[0]?.compilation + const edgeRuntimeErrors = [ + ...(clientCompilation?.errors.filter((e) => + isEdgeRuntimeCompiled(clientCompilation, e.module) + ) ?? []), + ...(result.stats[2]?.compilation.errors ?? []), + ] + + for (const error of edgeRuntimeErrors) { + // When using the web runtime, common Node.js native APIs are not available. + const moduleName = getUnresolvedModuleFromError(error.message) + if (!moduleName) continue + const err = new Error( - `Native Node.js APIs are not supported in the Edge Runtime. Found \`${moduleName}\` imported.\n\n` + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage(moduleName) ) as NextError err.code = 'EDGE_RUNTIME_UNSUPPORTED_API' throw err diff --git a/packages/next/build/output/store.ts b/packages/next/build/output/store.ts index 4c0af92680308..d2520f71f0155 100644 --- a/packages/next/build/output/store.ts +++ b/packages/next/build/output/store.ts @@ -1,7 +1,6 @@ import createStore from 'next/dist/compiled/unistore' import stripAnsi from 'next/dist/compiled/strip-ansi' import { flushAllTraces } from '../../trace' -import { getUnresolvedModuleFromError } from '../utils' import * as Log from './log' @@ -90,14 +89,6 @@ store.subscribe((state) => { } } - const moduleName = getUnresolvedModuleFromError(cleanError) - if (state.hasEdgeServer && moduleName) { - console.error( - `Native Node.js APIs are not supported in the Edge Runtime. Found \`${moduleName}\` imported.\n` - ) - return - } - // Ensure traces are flushed after each compile in development mode flushAllTraces() return diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index d9d7a058aaed6..aba3ea668bc6d 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -1,4 +1,5 @@ import type { NextConfigComplete, PageRuntime } from '../server/config-shared' +import type { webpack5 } from 'next/dist/compiled/webpack/webpack' import '../server/node-polyfill-fetch' import chalk from 'next/dist/compiled/chalk' @@ -20,6 +21,10 @@ import { SERVER_PROPS_SSG_CONFLICT, MIDDLEWARE_ROUTE, } from '../lib/constants' +import { + MIDDLEWARE_RUNTIME_WEBPACK, + MIDDLEWARE_SSR_RUNTIME_WEBPACK, +} from '../shared/lib/constants' import prettyBytes from '../lib/pretty-bytes' import { recursiveReadDir } from '../lib/recursive-readdir' import { getRouteMatcher, getRouteRegex } from '../shared/lib/router/utils' @@ -53,6 +58,11 @@ const fsStatGzip = (file: string) => { return (fileGzipStats[file] = getGzipSize.file(file)) } +const WEBPACK_EDGE_RUNTIMES = new Set([ + MIDDLEWARE_RUNTIME_WEBPACK, + MIDDLEWARE_SSR_RUNTIME_WEBPACK, +]) + const fileSize = async (file: string) => (await fs.stat(file)).size const fileStats: { [k: string]: Promise | undefined } = {} @@ -1129,7 +1139,7 @@ export function getUnresolvedModuleFromError( error: string ): string | undefined { const moduleErrorRegex = new RegExp( - `Module not found: Can't resolve '(\\w+)'` + `Module not found: Error: Can't resolve '(\\w+)'` ) const [, moduleName] = error.match(moduleErrorRegex) || [] return builtinModules.find((item: string) => item === moduleName) @@ -1272,3 +1282,39 @@ export function isReservedPage(page: string) { export function isCustomErrorPage(page: string) { return page === '/404' || page === '/500' } + +// FIX ME: it does not work for non-middleware edge functions +// since chunks don't contain runtime specified somehow +export function isEdgeRuntimeCompiled( + compilation: webpack5.Compilation, + module: any +) { + let isEdgeRuntime = false + + for (const chunk of compilation.chunkGraph.getModuleChunksIterable(module)) { + let runtimes: string[] + if (typeof chunk.runtime === 'string') { + runtimes = [chunk.runtime] + } else if (chunk.runtime) { + runtimes = [...chunk.runtime] + } else { + runtimes = [] + } + + if (runtimes.some((r) => WEBPACK_EDGE_RUNTIMES.has(r))) { + isEdgeRuntime = true + break + } + } + + return isEdgeRuntime +} + +export function getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage( + name: string +) { + return ( + `You're using a Node.js module (${name}) which is not supported in the Edge Runtime that Middleware uses.\n` + + 'Learn more: https://nextjs.org/docs/api-reference/edge-runtime' + ) +} diff --git a/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts b/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts index 5536ab4dca825..fb2c2389fbebc 100644 --- a/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts +++ b/packages/next/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts @@ -2,6 +2,11 @@ import Chalk from 'next/dist/compiled/chalk' import { SimpleWebpackError } from './simpleWebpackError' import { createOriginalStackFrame } from 'next/dist/compiled/@next/react-dev-overlay/middleware' import type { webpack5 } from 'next/dist/compiled/webpack/webpack' +import { + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage, + getUnresolvedModuleFromError, + isEdgeRuntimeCompiled, +} from '../../../utils' const chalk = new Chalk.constructor({ enabled: true }) @@ -98,7 +103,7 @@ export async function getNotFoundError( const frame = result.originalCodeFrame ?? '' - const message = + let message = chalk.red.bold('Module not found') + `: ${errorMessage}` + '\n' + @@ -107,6 +112,15 @@ export async function getNotFoundError( importTrace() + '\nhttps://nextjs.org/docs/messages/module-not-found' + if (isEdgeRuntimeCompiled(compilation, input.module)) { + const moduleName = getUnresolvedModuleFromError(input.message) + if (moduleName) { + message += + '\n\n' + + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage(moduleName) + } + } + return new SimpleWebpackError( `${chalk.cyan(fileName)}:${chalk.yellow( result.originalStackFrame.lineNumber?.toString() ?? '' diff --git a/packages/next/client/dev/error-overlay/format-webpack-messages.js b/packages/next/client/dev/error-overlay/format-webpack-messages.js index cc7f49d14e80b..a07fccc75c7f7 100644 --- a/packages/next/client/dev/error-overlay/format-webpack-messages.js +++ b/packages/next/client/dev/error-overlay/format-webpack-messages.js @@ -153,7 +153,11 @@ function formatWebpackMessages(json, verbose) { const formattedWarnings = json.warnings.map(function (message) { return formatMessage(message, verbose) }) - const result = { errors: formattedErrors, warnings: formattedWarnings } + const result = { + ...json, + errors: formattedErrors, + warnings: formattedWarnings, + } if (!verbose && result.errors.some(isLikelyASyntaxError)) { // If there are any syntax errors, show just them. result.errors = result.errors.filter(isLikelyASyntaxError) diff --git a/test/integration/middleware/with-builtin-module/test/index.test.js b/test/integration/middleware/with-builtin-module/test/index.test.js index a2705422da2ad..9ee83df152935 100644 --- a/test/integration/middleware/with-builtin-module/test/index.test.js +++ b/test/integration/middleware/with-builtin-module/test/index.test.js @@ -1,6 +1,7 @@ /* eslint-env jest */ import stripAnsi from 'next/dist/compiled/strip-ansi' +import { getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage } from 'next/dist/build/utils' import { join } from 'path' import { fetchViaHTTP, @@ -22,14 +23,16 @@ describe('Middleware importing Node.js built-in module', () => { return `Module not found: Can't resolve '${name}'` } - function getBuiltinApisNotSupported(name) { - return `Native Node.js APIs are not supported in the Edge Runtime. Found \`${name}\` imported.\n` + function escapeLF(s) { + return s.replaceAll('\n', '\\n') } describe('dev mode', () => { let output = '' - beforeAll(async () => { + // restart the app for every test since the latest error is not shown sometimes + beforeEach(async () => { + output = '' context.appPort = await findPort() context.app = await launchApp(context.appDir, context.appPort, { env: { __NEXT_TEST_WITH_DEVTOOL: 1 }, @@ -42,25 +45,31 @@ describe('Middleware importing Node.js built-in module', () => { }) }) - beforeEach(() => (output = '')) - afterAll(() => killApp(context.app)) + afterEach(() => killApp(context.app)) it('shows error when importing path module', async () => { const res = await fetchViaHTTP(context.appPort, '/using-path') + const text = await res.text() await waitFor(500) + const msg = getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('path') expect(res.status).toBe(500) expect(output).toContain(getModuleNotFound('path')) - expect(output).toContain(getBuiltinApisNotSupported('path')) + expect(output).toContain(msg) + expect(text).toContain(escapeLF(msg)) expect(stripAnsi(output)).toContain("import { basename } from 'path'") expect(output).not.toContain(WEBPACK_BREAKING_CHANGE) }) it('shows error when importing child_process module', async () => { const res = await fetchViaHTTP(context.appPort, '/using-child-process') + const text = await res.text() await waitFor(500) + const msg = + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('child_process') expect(res.status).toBe(500) expect(output).toContain(getModuleNotFound('child_process')) - expect(output).toContain(getBuiltinApisNotSupported('child_process')) + expect(output).toContain(msg) + expect(text).toContain(escapeLF(msg)) expect(stripAnsi(output)).toContain( "import { spawn } from 'child_process'" ) @@ -82,7 +91,7 @@ describe('Middleware importing Node.js built-in module', () => { expect(buildResult.stderr).toContain(getModuleNotFound('child_process')) expect(buildResult.stderr).toContain(getModuleNotFound('path')) expect(buildResult.stderr).toContain( - getBuiltinApisNotSupported('child_process') + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('child_process') ) expect(buildResult.stderr).not.toContain(WEBPACK_BREAKING_CHANGE) }) diff --git a/test/integration/middleware/without-builtin-module/pages/using-child-process-on-page/index.js b/test/integration/middleware/without-builtin-module/pages/using-child-process-on-page/index.js new file mode 100644 index 0000000000000..6cc59d6d2c7e1 --- /dev/null +++ b/test/integration/middleware/without-builtin-module/pages/using-child-process-on-page/index.js @@ -0,0 +1,6 @@ +import { spawn } from 'child_process' + +export default function Page() { + console.log(spawn('ls', ['-lh', '/usr'])) + return
ok
+} diff --git a/test/integration/middleware/without-builtin-module/pages/using-not-exist/_middleware.js b/test/integration/middleware/without-builtin-module/pages/using-not-exist/_middleware.js new file mode 100644 index 0000000000000..c9383b5d84408 --- /dev/null +++ b/test/integration/middleware/without-builtin-module/pages/using-not-exist/_middleware.js @@ -0,0 +1,7 @@ +import { NextResponse } from 'next/server' +import NotExist from 'not-exist' + +export async function middleware(request) { + console.log(new NotExist()) + return NextResponse.next() +} diff --git a/test/integration/middleware/without-builtin-module/test/index.test.js b/test/integration/middleware/without-builtin-module/test/index.test.js new file mode 100644 index 0000000000000..1a7c2f8a8f133 --- /dev/null +++ b/test/integration/middleware/without-builtin-module/test/index.test.js @@ -0,0 +1,105 @@ +/* eslint-env jest */ + +import stripAnsi from 'next/dist/compiled/strip-ansi' +import { getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage } from 'next/dist/build/utils' +import { join } from 'path' +import { + fetchViaHTTP, + findPort, + killApp, + launchApp, + nextBuild, + waitFor, +} from 'next-test-utils' + +const context = {} + +jest.setTimeout(1000 * 60 * 2) +context.appDir = join(__dirname, '../') + +describe('Middleware importing Node.js built-in module', () => { + function getModuleNotFound(name) { + return `Module not found: Can't resolve '${name}'` + } + + function escapeLF(s) { + return s.replaceAll('\n', '\\n') + } + + describe('dev mode', () => { + let output = '' + + // restart the app for every test since the latest error is not shown sometimes + beforeEach(async () => { + output = '' + context.appPort = await findPort() + context.app = await launchApp(context.appDir, context.appPort, { + env: { __NEXT_TEST_WITH_DEVTOOL: 1 }, + onStdout(msg) { + output += msg + }, + onStderr(msg) { + output += msg + }, + }) + }) + + afterEach(() => killApp(context.app)) + + it('does not show the not-supported error when importing non-node-builtin module', async () => { + const res = await fetchViaHTTP(context.appPort, '/using-not-exist') + expect(res.status).toBe(500) + + const text = await res.text() + await waitFor(500) + const msg = + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('not-exist') + expect(output).toContain(getModuleNotFound('not-exist')) + expect(output).not.toContain(msg) + expect(text).not.toContain(escapeLF(msg)) + }) + + it('does not show the not-supported error when importing child_process module on a page', async () => { + await fetchViaHTTP(context.appPort, '/using-child-process-on-page') + + // Need to request twice + // See: https://github.com/vercel/next.js/issues/36387 + const res = await fetchViaHTTP( + context.appPort, + '/using-child-process-on-page' + ) + + expect(res.status).toBe(500) + + const text = await res.text() + await waitFor(500) + const msg = + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('child_process') + expect(output).toContain(getModuleNotFound('child_process')) + expect(output).not.toContain(msg) + expect(text).not.toContain(escapeLF(msg)) + }) + }) + + describe('production mode', () => { + let buildResult + + beforeAll(async () => { + buildResult = await nextBuild(context.appDir, undefined, { + stderr: true, + stdout: true, + }) + }) + + it('should not have middleware error during build', () => { + expect(buildResult.stderr).toContain(getModuleNotFound('not-exist')) + expect(buildResult.stderr).toContain(getModuleNotFound('child_process')) + expect(buildResult.stderr).not.toContain( + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('not-exist') + ) + expect(buildResult.stderr).not.toContain( + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('child_process') + ) + }) + }) +}) diff --git a/test/integration/react-streaming-and-server-components/test/index.test.js b/test/integration/react-streaming-and-server-components/test/index.test.js index 4e526e8ede82d..8aae4202a209a 100644 --- a/test/integration/react-streaming-and-server-components/test/index.test.js +++ b/test/integration/react-streaming-and-server-components/test/index.test.js @@ -25,6 +25,7 @@ import rsc from './rsc' import streaming from './streaming' import basic from './basic' import runtime from './runtime' +import { getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage } from 'next/dist/build/utils' const rscAppPage = ` import Container from '../components/container.server' @@ -52,7 +53,7 @@ export default function Page500() { describe('Edge runtime - errors', () => { it('should warn user that native node APIs are not supported', async () => { const fsImportedErrorMessage = - 'Native Node.js APIs are not supported in the Edge Runtime. Found `dns` imported.' + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage('dns') const { stderr } = await nextBuild(nativeModuleTestAppDir, [], { stderr: true, })