diff --git a/packages/next/build/compiler.ts b/packages/next/build/compiler.ts index 074211a97dbf..41c7202ac9bf 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 4171a17f1983..1e2bd262d40c 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -78,7 +78,7 @@ import { eventPackageUsedInGetServerSideProps, } from '../telemetry/events' import { Telemetry } from '../telemetry/storage' -import { CompilerResult, runCompiler } from './compiler' +import { runCompiler } from './compiler' import { createEntrypoints, createPagesMapping, @@ -96,6 +96,7 @@ import { PageInfo, printCustomRoutes, printTreeView, + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage, getUnresolvedModuleFromError, copyTracedFiles, isReservedPage, @@ -135,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, @@ -164,7 +175,6 @@ export default async function build( // We enable concurrent features (Fizz-related rendering architecture) when // using React 18 or experimental. const hasReactRoot = !!process.env.__NEXT_REACT_ROOT - const hasConcurrentFeatures = hasReactRoot const hasServerComponents = hasReactRoot && !!config.experimental.serverComponents @@ -619,7 +629,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 () => { @@ -684,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], { @@ -704,6 +719,11 @@ export default async function build( ...serverResult.errors, ...(edgeServerResult?.errors || []), ], + stats: [ + clientResult.stats, + serverResult.stats, + edgeServerResult?.stats, + ], } } }) @@ -745,14 +765,18 @@ 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 err = new Error( - `Native Node.js APIs are not supported in the Edge Runtime. Found \`${moduleName}\` imported.\n\n` + const edgeRuntimeErrors = result.stats[2]?.compilation.errors ?? [] + + for (const err of edgeRuntimeErrors) { + // When using the web runtime, common Node.js native APIs are not available. + const moduleName = getUnresolvedModuleFromError(err.message) + if (!moduleName) continue + + const e = new Error( + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage(moduleName) ) as NextError - err.code = 'EDGE_RUNTIME_UNSUPPORTED_API' - throw err + e.code = 'EDGE_RUNTIME_UNSUPPORTED_API' + throw e } if ( diff --git a/packages/next/build/output/store.ts b/packages/next/build/output/store.ts index 4c0af9268030..d2520f71f015 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 70e29d4f8b87..cb0a154d6df9 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -1,4 +1,9 @@ -import type { NextConfigComplete, PageRuntime } from '../server/config-shared' +import type { + NextConfig, + 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 +25,7 @@ import { SERVER_PROPS_SSG_CONFLICT, MIDDLEWARE_ROUTE, } from '../lib/constants' +import { EDGE_RUNTIME_WEBPACK } from '../shared/lib/constants' import prettyBytes from '../lib/pretty-bytes' import { getRouteMatcher, getRouteRegex } from '../shared/lib/router/utils' import { isDynamicRoute } from '../shared/lib/router/utils/is-dynamic' @@ -40,6 +46,7 @@ import { Sema } from 'next/dist/compiled/async-sema' import { MiddlewareManifest } from './webpack/plugins/middleware-plugin' import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path' import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' +import { getPageRuntime } from './entries' const { builtinModules } = require('module') const RESERVED_PAGE = /^\/(_app|_error|_document|api(\/|$))/ @@ -1121,7 +1128,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) @@ -1264,3 +1271,41 @@ 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 async function isEdgeRuntimeCompiled( + compilation: webpack5.Compilation, + module: any, + config: NextConfig +) { + if (!module) return 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) => r === EDGE_RUNTIME_WEBPACK)) { + return true + } + } + + // Check the page runtime as well since we cannot detect the runtime from + // compilation when it's for the client part of edge function + return (await getPageRuntime(module.resource, config)) === 'edge' +} + +export function getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage( + name: string +) { + return ( + `You're using a Node.js module (${name}) which is not supported in the Edge Runtime.\n` + + 'Learn more: https://nextjs.org/docs/api-reference/edge-runtime' + ) +} diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 99fce88e38b8..fab5cbf8f8a9 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -1537,7 +1537,7 @@ export default async function getBaseWebpackConfig( isLikeServerless, }) })(), - new WellKnownErrorsPlugin(), + new WellKnownErrorsPlugin({ config }), isClient && new CopyFilePlugin({ filePath: require.resolve('./polyfills/polyfill-nomodule'), diff --git a/packages/next/build/webpack/plugins/wellknown-errors-plugin/index.ts b/packages/next/build/webpack/plugins/wellknown-errors-plugin/index.ts index c7de52cc80ac..14c1e4c774a1 100644 --- a/packages/next/build/webpack/plugins/wellknown-errors-plugin/index.ts +++ b/packages/next/build/webpack/plugins/wellknown-errors-plugin/index.ts @@ -1,7 +1,14 @@ import type { webpack5 as webpack } from 'next/dist/compiled/webpack/webpack' import { getModuleBuildError } from './webpackModuleError' +import { NextConfig } from '../../../../server/config-shared' export class WellKnownErrorsPlugin { + config: NextConfig + + constructor({ config }: { config: NextConfig }) { + this.config = config + } + apply(compiler: webpack.Compiler) { compiler.hooks.compilation.tap('WellKnownErrorsPlugin', (compilation) => { compilation.hooks.afterSeal.tapPromise( @@ -13,7 +20,8 @@ export class WellKnownErrorsPlugin { try { const moduleError = await getModuleBuildError( compilation, - err + err, + this.config ) if (moduleError !== false) { compilation.errors[i] = moduleError 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 5536ab4dca82..c95bc508e748 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,12 @@ 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' +import { NextConfig } from '../../../../server/config-shared' const chalk = new Chalk.constructor({ enabled: true }) @@ -48,7 +54,8 @@ function getModuleTrace(input: any, compilation: any) { export async function getNotFoundError( compilation: webpack5.Compilation, input: any, - fileName: string + fileName: string, + config: NextConfig ) { if (input.name !== 'ModuleNotFoundError') { return false @@ -98,7 +105,7 @@ export async function getNotFoundError( const frame = result.originalCodeFrame ?? '' - const message = + let message = chalk.red.bold('Module not found') + `: ${errorMessage}` + '\n' + @@ -107,6 +114,15 @@ export async function getNotFoundError( importTrace() + '\nhttps://nextjs.org/docs/messages/module-not-found' + const moduleName = getUnresolvedModuleFromError(input.message) + if (moduleName) { + if (await isEdgeRuntimeCompiled(compilation, input.module, config)) { + message += + '\n\n' + + getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage(moduleName) + } + } + return new SimpleWebpackError( `${chalk.cyan(fileName)}:${chalk.yellow( result.originalStackFrame.lineNumber?.toString() ?? '' diff --git a/packages/next/build/webpack/plugins/wellknown-errors-plugin/webpackModuleError.ts b/packages/next/build/webpack/plugins/wellknown-errors-plugin/webpackModuleError.ts index dc7e0e3cf36f..db45436f23a8 100644 --- a/packages/next/build/webpack/plugins/wellknown-errors-plugin/webpackModuleError.ts +++ b/packages/next/build/webpack/plugins/wellknown-errors-plugin/webpackModuleError.ts @@ -7,6 +7,7 @@ import { getScssError } from './parseScss' import { getNotFoundError } from './parseNotFoundError' import { SimpleWebpackError } from './simpleWebpackError' import isError from '../../../../lib/is-error' +import { NextConfig } from '../../../../server/config-shared' function getFileData( compilation: webpack.Compilation, @@ -42,7 +43,8 @@ function getFileData( export async function getModuleBuildError( compilation: webpack.Compilation, - input: any + input: any, + config: NextConfig ): Promise { if ( !( @@ -62,7 +64,8 @@ export async function getModuleBuildError( const notFoundError = await getNotFoundError( compilation, input, - sourceFilename + sourceFilename, + config ) if (notFoundError !== false) { return notFoundError 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 f073ca91da3d..54214c9e9cdb 100644 --- a/packages/next/client/dev/error-overlay/format-webpack-messages.js +++ b/packages/next/client/dev/error-overlay/format-webpack-messages.js @@ -163,7 +163,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 a2705422da2a..8dc72b64ff07 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,17 @@ 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.replace(/\n/g, '\\n') } describe('dev mode', () => { let output = '' - beforeAll(async () => { + // restart the app for every test since the latest error is not shown sometimes + // See https://github.com/vercel/next.js/issues/36575 + beforeEach(async () => { + output = '' context.appPort = await findPort() context.app = await launchApp(context.appDir, context.appPort, { env: { __NEXT_TEST_WITH_DEVTOOL: 1 }, @@ -42,25 +46,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 +92,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 000000000000..7c9f7f8b91bf --- /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() { + 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 000000000000..b1699a1acb2b --- /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) { + 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 000000000000..b31c60fc5671 --- /dev/null +++ b/test/integration/middleware/without-builtin-module/test/index.test.js @@ -0,0 +1,100 @@ +/* eslint-env jest */ + +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.replace(/\n/g, '\\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('child_process')) + 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 0d361be78ee2..2e4e62b8acd6 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, })