Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix next node buildin module error message for edge runtime #36434

Merged
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/next/build/compiler.ts
Expand Up @@ -5,6 +5,7 @@ import { Span } from '../trace'
export type CompilerResult = {
errors: webpack5.StatsError[]
warnings: webpack5.StatsError[]
stats: webpack5.Stats | undefined
}

function generateStats(
Expand Down Expand Up @@ -54,14 +55,17 @@ export function runCompiler(
return resolve({
errors: [{ message: reason, details: (err as any).details }],
warnings: [],
stats,
})
}
return reject(err)
} else if (!stats) throw new Error('No Stats from webpack')

const result = webpackCloseSpan
.traceChild('webpack-generate-error-stats')
.traceFn(() => generateStats({ errors: [], warnings: [] }, stats))
.traceFn(() =>
generateStats({ errors: [], warnings: [], stats }, stats)
)
return resolve(result)
})
})
Expand Down
44 changes: 34 additions & 10 deletions packages/next/build/index.ts
Expand Up @@ -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,
Expand All @@ -96,6 +96,7 @@ import {
PageInfo,
printCustomRoutes,
printTreeView,
getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage,
getUnresolvedModuleFromError,
copyTracedFiles,
isReservedPage,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -615,7 +625,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 () => {
Expand Down Expand Up @@ -680,6 +694,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], {
Expand All @@ -700,6 +715,11 @@ export default async function build(
...serverResult.errors,
...(edgeServerResult?.errors || []),
],
stats: [
clientResult.stats,
serverResult.stats,
edgeServerResult?.stats,
],
}
}
})
Expand Down Expand Up @@ -741,14 +761,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 (
Expand Down
9 changes: 0 additions & 9 deletions 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'

Expand Down Expand Up @@ -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
Expand Down
49 changes: 47 additions & 2 deletions 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'
Expand All @@ -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'
Expand All @@ -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(\/|$))/
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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'
)
}
2 changes: 1 addition & 1 deletion packages/next/build/webpack-config.ts
Expand Up @@ -1537,7 +1537,7 @@ export default async function getBaseWebpackConfig(
isLikeServerless,
})
})(),
new WellKnownErrorsPlugin(),
new WellKnownErrorsPlugin({ config }),
isClient &&
new CopyFilePlugin({
filePath: require.resolve('./polyfills/polyfill-nomodule'),
Expand Down
@@ -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(
Expand All @@ -13,7 +20,8 @@ export class WellKnownErrorsPlugin {
try {
const moduleError = await getModuleBuildError(
compilation,
err
err,
this.config
)
if (moduleError !== false) {
compilation.errors[i] = moduleError
Expand Down
Expand Up @@ -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 })

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -98,7 +105,7 @@ export async function getNotFoundError(

const frame = result.originalCodeFrame ?? ''

const message =
let message =
chalk.red.bold('Module not found') +
`: ${errorMessage}` +
'\n' +
Expand All @@ -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() ?? ''
Expand Down
Expand Up @@ -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,
Expand Down Expand Up @@ -42,7 +43,8 @@ function getFileData(

export async function getModuleBuildError(
compilation: webpack.Compilation,
input: any
input: any,
config: NextConfig
): Promise<SimpleWebpackError | false> {
if (
!(
Expand All @@ -62,7 +64,8 @@ export async function getModuleBuildError(
const notFoundError = await getNotFoundError(
compilation,
input,
sourceFilename
sourceFilename,
config
)
if (notFoundError !== false) {
return notFoundError
Expand Down
Expand Up @@ -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)
Expand Down