Skip to content

Commit

Permalink
Fix next node buildin module error message for edge runtime (#36434)
Browse files Browse the repository at this point in the history
- improve the message for importing node builtin module on edge runtime
- fix to show the message on overlay of error browser with `next dev`
- fix #36237

The message is NOT shown when using edge runtime (not middleware) since I cannot find a way to detect a webpack compilation is for edge runtime.

## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `yarn lint`
  • Loading branch information
nkzawa committed May 2, 2022
1 parent c905fda commit 9e53af8
Show file tree
Hide file tree
Showing 14 changed files with 257 additions and 38 deletions.
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 @@ -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 () => {
Expand Down Expand Up @@ -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], {
Expand All @@ -704,6 +719,11 @@ export default async function build(
...serverResult.errors,
...(edgeServerResult?.errors || []),
],
stats: [
clientResult.stats,
serverResult.stats,
edgeServerResult?.stats,
],
}
}
})
Expand Down Expand Up @@ -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 (
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

0 comments on commit 9e53af8

Please sign in to comment.