Skip to content

Commit

Permalink
fix error message for using node buildin modules on edge functions
Browse files Browse the repository at this point in the history
  • Loading branch information
nkzawa committed Apr 25, 2022
1 parent c33a9e4 commit 04eff87
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 29 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
45 changes: 38 additions & 7 deletions packages/next/build/index.ts
Expand Up @@ -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,
Expand All @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 })
Expand All @@ -701,6 +717,11 @@ export default async function build(
...serverResult.errors,
...(edgeServerResult?.errors || []),
],
stats: [
clientResult.stats,
serverResult.stats,
edgeServerResult?.stats,
],
}
}
})
Expand Down Expand Up @@ -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
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
48 changes: 47 additions & 1 deletion 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'
Expand All @@ -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'
Expand Down Expand Up @@ -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<number> | undefined } = {}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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'
)
}
Expand Up @@ -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 })

Expand Down Expand Up @@ -98,7 +103,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 +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() ?? ''
Expand Down
Expand Up @@ -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)
Expand Down
25 changes: 17 additions & 8 deletions 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,
Expand All @@ -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 },
Expand All @@ -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'"
)
Expand All @@ -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)
})
Expand Down
@@ -0,0 +1,6 @@
import { spawn } from 'child_process'

export default function Page() {
console.log(spawn('ls', ['-lh', '/usr']))
return <div>ok</div>
}
@@ -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()
}

0 comments on commit 04eff87

Please sign in to comment.