Skip to content

Commit 6e2c382

Browse files
authoredJul 6, 2022
feat: build edge functions with node.js modules and fail at runtime (#38234)
## What's in there? The Edge runtime [does not support Node.js modules](https://edge-runtime.vercel.app/features/available-apis#unsupported-apis). When building Next.js application, we currently fail the build when detecting node.js module imported from middleware. This is an blocker for using code that is conditionally loading node.js modules (based on platform/env detection), as @cramforce reported. This PR implements a new strategy where: - we can build such middleware/Edge API route code **with a warning** - we fail at run time, with graceful errors in dev (console & react-dev-overlay error) - we fail at run time, with console errors in production ## How to test? All cases are covered with integration tests. To try them live, create a simple app with a page, a `middleware.js` file and a `pages/api/route.js`file. Here are iconic examples: ### node.js modules ```js // middleware.js import { NextResponse } from 'next/server' // static import { basename } from 'path' export default async function middleware() { // dynamic const { basename } = await import('path') basename() return NextResponse.next() } export const config = { matcher: '/' } ``` ```js // pags/api/route.js // static import { isAbsolute } from 'path' export default async function handle() { // dynamic const { isAbsolute } = await import('path') return Response.json({ useNodeModule: isAbsolute('/test') }) } export const config = { runtime: 'experimental-edge' } ``` Desired error (+ source code highlight in dev): > The edge runtime does not support Node.js 'path' module Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime Desired warning at build time: > A Node.js module is loaded ('path' at line 2) which is not supported in the Edge Runtime. Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime - [x] in dev middleware, static, shows desired error on stderr - [x] in dev route, static, shows desired error on stderr - [x] in dev middleware, dynamic, shows desired error on stderr - [x] in dev route, dynamic, shows desired error on stderr - [x] in dev middleware, static, shows desired error on react error overlay - [x] in dev route, static, shows desired error on react error overlay - [x] in dev middleware, dynamic, shows desired error on react error overlay - [x] in dev route, dynamic, shows desired error on react error overlay - [x] builds middleware successfully, shows build warning, shows desired error on stderr on call - [x] builds route successfully, shows build warning, shows desired error on stderr on call ### 3rd party modules not found ```js // middleware.js import { NextResponse } from 'next/server' // static import Unknown from 'unknown' export default async function middleware() { // dynamic const Unknown = await import('unknown') new Unknown() return NextResponse.next() } ``` export const config = { matcher: '/' } ``` ```js // pags/api/route.js // static import Unknown from 'unknown' export default async function handle() { // dynamic const Unknown = await import('unknown') return Response.json({ use3rdPartyModule: Unknown() }) } export const config = { runtime: 'experimental-edge' } ``` Desired error (+ source code highlight in dev): > Module not found: Can't resolve 'does-not-exist' Learn More: https://nextjs.org/docs/messages/module-not-found - [x] in dev middleware, static, shows desired error on stderr - [x] in dev route, static, shows desired error on stderr - [x] in dev middleware, dynamic, shows desired error on stderr - [x] in dev route, dynamic, shows desired error on stderr - [x] in dev middleware, static, shows desired error on react error overlay - [x] in dev route, static, shows desired error on react error overlay - [x] in dev middleware, dynamic, shows desired error on react error overlay - [x] in dev route, dynamic, shows desired error on react error overlay - [x] fails to build middleware, with desired error on stderr - [x] fails to build route, with desired error on stderr ### unused node.js modules ```js // middleware.js import { NextResponse } from 'next/server' export default async function middleware() { if (process.exit) { const { basename } = await import('path') basename() } return NextResponse.next() } ``` ```js // pags/api/route.js export default async function handle() { if (process.exit) { const { basename } = await import('path') basename() } return Response.json({ useNodeModule: false }) } export const config = { runtime: 'experimental-edge' } ``` Desired warning at build time: > A Node.js module is loaded ('path' at line 2) which is not supported in the Edge Runtime. Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime - [x] invoke middleware in dev with no error - [x] invoke route in dev with no error - [x] builds successfully, shows build warning, invoke middleware with no error - [x] builds successfully, shows build warning, invoke api-route with no error ## Notes to reviewers The strategy to implement this feature is to leverages webpack [externals](https://webpack.js.org/configuration/externals/#externals) and run a global `__unsupported_module()` function when using a node.js module from edge function's code. For the record, I tried using [webpack resolve.fallback](https://webpack.js.org/configuration/resolve/#resolvefallback) and [Webpack.IgnorePlugin](https://webpack.js.org/plugins/ignore-plugin/) but they do not allow throwing proper errors at runtime that would contain the loaded module name for reporting. `__unsupported_module()` is defined in `EdgeRuntime`, and returns a proxy that's throw on use (whether it's property access, function call, new operator... synchronous & promise-based styles). However there's an issue with error reporting: webpack does not includes the import lines in the generated sourcemaps, preventing from displaying useful errors. I extended our middleware-plugin to supplement the sourcemaps (when analyzing edge function code, it saves which module is imported from which file, together with line/column/source) The react-dev-overlay was adapted to look for this additional information when the caught error relates to modules, instead of looking at sourcemaps. I removed the previous mechanism (built by @nkzawa ) which caught webpack errors at built time to change the displayed error message (files `next/build/index.js`, `next/build/utils.ts` and `wellknown-errors-plugin`)
1 parent 0f4333a commit 6e2c382

File tree

33 files changed

+1024
-470
lines changed

33 files changed

+1024
-470
lines changed
 

‎errors/manifest.json

+4
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,10 @@
707707
{
708708
"title": "middleware-dynamic-wasm-compilation",
709709
"path": "/errors/middleware-dynamic-wasm-compilation.md"
710+
},
711+
{
712+
"title": "node-module-in-edge-runtime",
713+
"path": "/errors/node-module-in-edge-runtime.md"
710714
}
711715
]
712716
}

‎errors/node-module-in-edge-runtime.md

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Using Node.js module in Edge runtime
2+
3+
#### Why This Error Occurred
4+
5+
The code in your [Middleware][middleware] or your [Edge API routes][routes] is using a feature from Node.js runtime.
6+
7+
However, the Edge Runtime does not support [Node.js APIs and globals][node-primitives].
8+
9+
#### Possible Ways to Fix It
10+
11+
When it runs in dev mode, your application will show in the console, and in your browser, which file is importing and using an unsupported module.
12+
This module must be avoided: either by not importing it, or by replacing it with a polyfill.
13+
14+
Please note your code can import Node.js modules **as long as they are not used**.
15+
The following example builds and works at runtime:
16+
17+
```ts
18+
import { NextResponse } from 'next/server'
19+
import { spawn } from 'child_process'
20+
21+
export default async function middleware() {
22+
if (process.version) {
23+
spawn('ls', ['-lh'])
24+
}
25+
return NextResponse.next()
26+
}
27+
```
28+
29+
Dynamic imports in unreachable code path is also supported.
30+
31+
### Useful Links
32+
33+
- [Edge runtime supported APIs and primitives][edge-primitives]
34+
- [Next.js Middleware][middleware]
35+
36+
[middleware]: https://nextjs.org/docs/advanced-features/middleware
37+
[routes]: https://nextjs.org/docs/api-routes/edge-api-routes
38+
[node-primitives]: https://nodejs.org/api/index.html
39+
[edge-primitives]: https://edge-runtime.vercel.app/features/available-apis

‎packages/next/build/entries.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -482,8 +482,8 @@ export function finalizeEntrypoint({
482482
? { import: value }
483483
: value
484484

485+
const isApi = name.startsWith('pages/api/')
485486
if (compilerType === 'server') {
486-
const isApi = name.startsWith('pages/api/')
487487
return {
488488
publicPath: isApi ? '' : undefined,
489489
runtime: isApi ? 'webpack-api-runtime' : 'webpack-runtime',
@@ -494,7 +494,7 @@ export function finalizeEntrypoint({
494494

495495
if (compilerType === 'edge-server') {
496496
return {
497-
layer: isMiddlewareFilename(name) ? 'middleware' : undefined,
497+
layer: isMiddlewareFilename(name) || isApi ? 'middleware' : undefined,
498498
library: { name: ['_ENTRIES', `middleware_[name]`], type: 'assign' },
499499
runtime: EDGE_RUNTIME_WEBPACK,
500500
asyncChunks: false,

‎packages/next/build/index.ts

-16
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,6 @@ import {
9090
PageInfo,
9191
printCustomRoutes,
9292
printTreeView,
93-
getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage,
94-
getUnresolvedModuleFromError,
9593
copyTracedFiles,
9694
isReservedPage,
9795
isServerComponentPage,
@@ -874,20 +872,6 @@ export default async function build(
874872
console.error(error)
875873
console.error()
876874

877-
const edgeRuntimeErrors = result.stats[2]?.compilation.errors ?? []
878-
879-
for (const err of edgeRuntimeErrors) {
880-
// When using the web runtime, common Node.js native APIs are not available.
881-
const moduleName = getUnresolvedModuleFromError(err.message)
882-
if (!moduleName) continue
883-
884-
const e = new Error(
885-
getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage(moduleName)
886-
) as NextError
887-
e.code = 'EDGE_RUNTIME_UNSUPPORTED_API'
888-
throw e
889-
}
890-
891875
if (
892876
error.indexOf('private-next-pages') > -1 ||
893877
error.indexOf('__next_polyfill__') > -1

‎packages/next/build/utils.ts

+1-62
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import type {
2-
NextConfig,
3-
NextConfigComplete,
4-
ServerRuntime,
5-
} from '../server/config-shared'
6-
import type { webpack5 } from 'next/dist/compiled/webpack/webpack'
1+
import type { NextConfigComplete, ServerRuntime } from '../server/config-shared'
72

83
import '../server/node-polyfill-fetch'
94
import chalk from 'next/dist/compiled/chalk'
@@ -26,7 +21,6 @@ import {
2621
MIDDLEWARE_FILENAME,
2722
SERVER_RUNTIME,
2823
} from '../lib/constants'
29-
import { EDGE_RUNTIME_WEBPACK } from '../shared/lib/constants'
3024
import prettyBytes from '../lib/pretty-bytes'
3125
import { getRouteRegex } from '../shared/lib/router/utils/route-regex'
3226
import { getRouteMatcher } from '../shared/lib/router/utils/route-matcher'
@@ -47,9 +41,7 @@ import { Sema } from 'next/dist/compiled/async-sema'
4741
import { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
4842
import { denormalizePagePath } from '../shared/lib/page-path/denormalize-page-path'
4943
import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
50-
import { getPageStaticInfo } from './analysis/get-page-static-info'
5144

52-
const { builtinModules } = require('module')
5345
const RESERVED_PAGE = /^\/(_app|_error|_document|api(\/|$))/
5446
const fileGzipStats: { [k: string]: Promise<number> | undefined } = {}
5547
const fsStatGzip = (file: string) => {
@@ -1120,16 +1112,6 @@ export function isServerComponentPage(
11201112
})
11211113
}
11221114

1123-
export function getUnresolvedModuleFromError(
1124-
error: string
1125-
): string | undefined {
1126-
const moduleErrorRegex = new RegExp(
1127-
`Module not found: Error: Can't resolve '(\\w+)'`
1128-
)
1129-
const [, moduleName] = error.match(moduleErrorRegex) || []
1130-
return builtinModules.find((item: string) => item === moduleName)
1131-
}
1132-
11331115
export async function copyTracedFiles(
11341116
dir: string,
11351117
distDir: string,
@@ -1268,49 +1250,6 @@ export function isCustomErrorPage(page: string) {
12681250
return page === '/404' || page === '/500'
12691251
}
12701252

1271-
// FIX ME: it does not work for non-middleware edge functions
1272-
// since chunks don't contain runtime specified somehow
1273-
export async function isEdgeRuntimeCompiled(
1274-
compilation: webpack5.Compilation,
1275-
module: any,
1276-
config: NextConfig
1277-
) {
1278-
if (!module) return false
1279-
1280-
for (const chunk of compilation.chunkGraph.getModuleChunksIterable(module)) {
1281-
let runtimes: string[]
1282-
if (typeof chunk.runtime === 'string') {
1283-
runtimes = [chunk.runtime]
1284-
} else if (chunk.runtime) {
1285-
runtimes = [...chunk.runtime]
1286-
} else {
1287-
runtimes = []
1288-
}
1289-
1290-
if (runtimes.some((r) => r === EDGE_RUNTIME_WEBPACK)) {
1291-
return true
1292-
}
1293-
}
1294-
1295-
const staticInfo = await getPageStaticInfo({
1296-
pageFilePath: module.resource,
1297-
nextConfig: config,
1298-
})
1299-
1300-
// Check the page runtime as well since we cannot detect the runtime from
1301-
// compilation when it's for the client part of edge function
1302-
return staticInfo.runtime === SERVER_RUNTIME.edge
1303-
}
1304-
1305-
export function getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage(
1306-
name: string
1307-
) {
1308-
return (
1309-
`You're using a Node.js module (${name}) which is not supported in the Edge Runtime.\n` +
1310-
'Learn more: https://nextjs.org/docs/api-reference/edge-runtime'
1311-
)
1312-
}
1313-
13141253
export function isMiddlewareFile(file: string) {
13151254
return (
13161255
file === `/${MIDDLEWARE_FILENAME}` || file === `/src/${MIDDLEWARE_FILENAME}`

‎packages/next/build/webpack-config.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ import { NextConfigComplete } from '../server/config-shared'
3434
import { finalizeEntrypoint } from './entries'
3535
import * as Log from './output/log'
3636
import { build as buildConfiguration } from './webpack/config'
37-
import MiddlewarePlugin from './webpack/plugins/middleware-plugin'
37+
import MiddlewarePlugin, {
38+
handleWebpackExtenalForEdgeRuntime,
39+
} from './webpack/plugins/middleware-plugin'
3840
import BuildManifestPlugin from './webpack/plugins/build-manifest-plugin'
3941
import { JsConfigPathsPlugin } from './webpack/plugins/jsconfig-paths-plugin'
4042
import { DropClientPage } from './webpack/plugins/next-drop-client-page-plugin'
@@ -976,6 +978,7 @@ export default async function getBaseWebpackConfig(
976978
'next/dist/compiled/chalk': '{}',
977979
'react-dom': '{}',
978980
},
981+
handleWebpackExtenalForEdgeRuntime,
979982
]
980983
: []),
981984
]
@@ -1672,7 +1675,7 @@ export default async function getBaseWebpackConfig(
16721675
isLikeServerless,
16731676
})
16741677
})(),
1675-
new WellKnownErrorsPlugin({ config }),
1678+
new WellKnownErrorsPlugin(),
16761679
isClient &&
16771680
new CopyFilePlugin({
16781681
filePath: require.resolve('./polyfills/polyfill-nomodule'),

‎packages/next/build/webpack/loaders/get-module-build-info.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export function getModuleBuildInfo(webpackModule: webpack5.Module) {
1313
nextWasmMiddlewareBinding?: WasmBinding
1414
usingIndirectEval?: boolean | Set<string>
1515
route?: RouteMeta
16+
importLocByPath?: Map<string, any>
1617
}
1718
}
1819

‎packages/next/build/webpack/plugins/middleware-plugin.ts

+117-34
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,18 @@ export default class MiddlewarePlugin {
9696
}
9797
}
9898

99+
export async function handleWebpackExtenalForEdgeRuntime({
100+
request,
101+
contextInfo,
102+
}: {
103+
request: string
104+
contextInfo: any
105+
}) {
106+
if (contextInfo.issuerLayer === 'middleware' && isNodeJsModule(request)) {
107+
return `root __import_unsupported('${request}')`
108+
}
109+
}
110+
99111
function getCodeAnalizer(params: {
100112
dev: boolean
101113
compiler: webpack5.Compiler
@@ -256,12 +268,13 @@ function getCodeAnalizer(params: {
256268
!isNullLiteral(firstParameter) &&
257269
!isUndefinedIdentifier(firstParameter)
258270
) {
259-
const error = new wp.WebpackError(
260-
`Your middleware is returning a response body (line: ${node.loc.start.line}), which is not supported. Learn more: https://nextjs.org/docs/messages/returning-response-body-in-middleware`
261-
)
262-
error.name = NAME
263-
error.module = parser.state.current
264-
error.loc = node.loc
271+
const error = buildWebpackError({
272+
message: `Middleware is returning a response body (line: ${node.loc.start.line}), which is not supported.
273+
Learn more: https://nextjs.org/docs/messages/returning-response-body-in-middleware`,
274+
compilation,
275+
parser,
276+
...node,
277+
})
265278
if (dev) {
266279
compilation.warnings.push(error)
267280
} else {
@@ -270,6 +283,40 @@ function getCodeAnalizer(params: {
270283
}
271284
}
272285

286+
/**
287+
* Handler to store original source location of static and dynamic imports into module's buildInfo.
288+
*/
289+
const handleImport = (node: any) => {
290+
if (isInMiddlewareLayer(parser) && node.source?.value && node?.loc) {
291+
const { module, source } = parser.state
292+
const buildInfo = getModuleBuildInfo(module)
293+
if (!buildInfo.importLocByPath) {
294+
buildInfo.importLocByPath = new Map()
295+
}
296+
297+
const importedModule = node.source.value?.toString()!
298+
buildInfo.importLocByPath.set(importedModule, {
299+
sourcePosition: {
300+
...node.loc.start,
301+
source: module.identifier(),
302+
},
303+
sourceContent: source.toString(),
304+
})
305+
306+
if (!dev && isNodeJsModule(importedModule)) {
307+
compilation.warnings.push(
308+
buildWebpackError({
309+
message: `A Node.js module is loaded ('${importedModule}' at line ${node.loc.start.line}) which is not supported in the Edge Runtime.
310+
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`,
311+
compilation,
312+
parser,
313+
...node,
314+
})
315+
)
316+
}
317+
}
318+
}
319+
273320
/**
274321
* A noop handler to skip analyzing some cases.
275322
* Order matters: for it to work, it must be registered first
@@ -295,6 +342,8 @@ function getCodeAnalizer(params: {
295342
hooks.new.for('NextResponse').tap(NAME, handleNewResponseExpression)
296343
hooks.callMemberChain.for('process').tap(NAME, handleCallMemberChain)
297344
hooks.expressionMemberChain.for('process').tap(NAME, handleCallMemberChain)
345+
hooks.importCall.tap(NAME, handleImport)
346+
hooks.import.tap(NAME, handleImport)
298347

299348
/**
300349
* Support static analyzing environment variables through
@@ -388,18 +437,19 @@ function getExtractMetadata(params: {
388437
continue
389438
}
390439

391-
const error = new wp.WebpackError(
392-
`Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Middleware ${entryName}${
393-
typeof buildInfo.usingIndirectEval !== 'boolean'
394-
? `\nUsed by ${Array.from(buildInfo.usingIndirectEval).join(
395-
', '
396-
)}`
397-
: ''
398-
}`
440+
compilation.errors.push(
441+
buildWebpackError({
442+
message: `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Middleware ${entryName}${
443+
typeof buildInfo.usingIndirectEval !== 'boolean'
444+
? `\nUsed by ${Array.from(buildInfo.usingIndirectEval).join(
445+
', '
446+
)}`
447+
: ''
448+
}`,
449+
entryModule,
450+
compilation,
451+
})
399452
)
400-
401-
error.module = entryModule
402-
compilation.errors.push(error)
403453
}
404454

405455
/**
@@ -564,14 +614,18 @@ function registerUnsupportedApiHooks(
564614
parser: webpack5.javascript.JavascriptParser,
565615
compilation: webpack5.Compilation
566616
) {
567-
const { WebpackError } = compilation.compiler.webpack
568617
for (const expression of EDGE_UNSUPPORTED_NODE_APIS) {
569618
const warnForUnsupportedApi = (node: any) => {
570619
if (!isInMiddlewareLayer(parser)) {
571620
return
572621
}
573622
compilation.warnings.push(
574-
makeUnsupportedApiError(WebpackError, parser, expression, node.loc)
623+
buildUnsupportedApiError({
624+
compilation,
625+
parser,
626+
apiName: expression,
627+
...node,
628+
})
575629
)
576630
return true
577631
}
@@ -590,12 +644,12 @@ function registerUnsupportedApiHooks(
590644
return
591645
}
592646
compilation.warnings.push(
593-
makeUnsupportedApiError(
594-
WebpackError,
647+
buildUnsupportedApiError({
648+
compilation,
595649
parser,
596-
`process.${callee}`,
597-
node.loc
598-
)
650+
apiName: `process.${callee}`,
651+
...node,
652+
})
599653
)
600654
return true
601655
}
@@ -608,18 +662,43 @@ function registerUnsupportedApiHooks(
608662
.tap(NAME, warnForUnsupportedProcessApi)
609663
}
610664

611-
function makeUnsupportedApiError(
612-
WebpackError: typeof webpack5.WebpackError,
613-
parser: webpack5.javascript.JavascriptParser,
614-
name: string,
665+
function buildUnsupportedApiError({
666+
apiName,
667+
loc,
668+
...rest
669+
}: {
670+
apiName: string
615671
loc: any
616-
) {
617-
const error = new WebpackError(
618-
`You're using a Node.js API (${name} at line: ${loc.start.line}) which is not supported in the Edge Runtime that Middleware uses.
619-
Learn more: https://nextjs.org/docs/api-reference/edge-runtime`
620-
)
672+
compilation: webpack5.Compilation
673+
parser: webpack5.javascript.JavascriptParser
674+
}) {
675+
return buildWebpackError({
676+
message: `A Node.js API is used (${apiName} at line: ${loc.start.line}) which is not supported in the Edge Runtime.
677+
Learn more: https://nextjs.org/docs/api-reference/edge-runtime`,
678+
loc,
679+
...rest,
680+
})
681+
}
682+
683+
function buildWebpackError({
684+
message,
685+
loc,
686+
compilation,
687+
entryModule,
688+
parser,
689+
}: {
690+
message: string
691+
loc?: any
692+
compilation: webpack5.Compilation
693+
entryModule?: webpack5.Module
694+
parser?: webpack5.javascript.JavascriptParser
695+
}) {
696+
const error = new compilation.compiler.webpack.WebpackError(message)
621697
error.name = NAME
622-
error.module = parser.state.current
698+
const module = entryModule ?? parser?.state.current
699+
if (module) {
700+
error.module = module
701+
}
623702
error.loc = loc
624703
return error
625704
}
@@ -653,3 +732,7 @@ function isProcessEnvMemberExpression(memberExpression: any): boolean {
653732
memberExpression.property.name === 'env'))
654733
)
655734
}
735+
736+
function isNodeJsModule(moduleName: string) {
737+
return require('module').builtinModules.includes(moduleName)
738+
}
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,27 @@
11
import type { webpack5 as webpack } from 'next/dist/compiled/webpack/webpack'
2-
import type { NextConfig } from '../../../../server/config-shared'
32

43
import { getModuleBuildError } from './webpackModuleError'
54

5+
const NAME = 'WellKnownErrorsPlugin'
66
export class WellKnownErrorsPlugin {
7-
config: NextConfig
8-
9-
constructor({ config }: { config: NextConfig }) {
10-
this.config = config
11-
}
12-
137
apply(compiler: webpack.Compiler) {
14-
compiler.hooks.compilation.tap('WellKnownErrorsPlugin', (compilation) => {
15-
compilation.hooks.afterSeal.tapPromise(
16-
'WellKnownErrorsPlugin',
17-
async () => {
18-
if (compilation.errors?.length) {
19-
await Promise.all(
20-
compilation.errors.map(async (err, i) => {
21-
try {
22-
const moduleError = await getModuleBuildError(
23-
compilation,
24-
err,
25-
this.config
26-
)
27-
if (moduleError !== false) {
28-
compilation.errors[i] = moduleError
29-
}
30-
} catch (e) {
31-
console.log(e)
8+
compiler.hooks.compilation.tap(NAME, (compilation) => {
9+
compilation.hooks.afterSeal.tapPromise(NAME, async () => {
10+
if (compilation.errors?.length) {
11+
await Promise.all(
12+
compilation.errors.map(async (err, i) => {
13+
try {
14+
const moduleError = await getModuleBuildError(compilation, err)
15+
if (moduleError !== false) {
16+
compilation.errors[i] = moduleError
3217
}
33-
})
34-
)
35-
}
18+
} catch (e) {
19+
console.log(e)
20+
}
21+
})
22+
)
3623
}
37-
)
24+
})
3825
})
3926
}
4027
}

‎packages/next/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts

+1-17
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,6 @@ import Chalk from 'next/dist/compiled/chalk'
22
import { SimpleWebpackError } from './simpleWebpackError'
33
import { createOriginalStackFrame } from 'next/dist/compiled/@next/react-dev-overlay/dist/middleware'
44
import type { webpack5 } from 'next/dist/compiled/webpack/webpack'
5-
import {
6-
getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage,
7-
getUnresolvedModuleFromError,
8-
isEdgeRuntimeCompiled,
9-
} from '../../../utils'
10-
import { NextConfig } from '../../../../server/config-shared'
115

126
const chalk = new Chalk.constructor({ enabled: true })
137

@@ -54,8 +48,7 @@ function getModuleTrace(input: any, compilation: any) {
5448
export async function getNotFoundError(
5549
compilation: webpack5.Compilation,
5650
input: any,
57-
fileName: string,
58-
config: NextConfig
51+
fileName: string
5952
) {
6053
if (input.name !== 'ModuleNotFoundError') {
6154
return false
@@ -114,15 +107,6 @@ export async function getNotFoundError(
114107
importTrace() +
115108
'\nhttps://nextjs.org/docs/messages/module-not-found'
116109

117-
const moduleName = getUnresolvedModuleFromError(input.message)
118-
if (moduleName) {
119-
if (await isEdgeRuntimeCompiled(compilation, input.module, config)) {
120-
message +=
121-
'\n\n' +
122-
getNodeBuiltinModuleNotSupportedInEdgeRuntimeMessage(moduleName)
123-
}
124-
}
125-
126110
return new SimpleWebpackError(
127111
`${chalk.cyan(fileName)}:${chalk.yellow(
128112
result.originalStackFrame.lineNumber?.toString() ?? ''

‎packages/next/build/webpack/plugins/wellknown-errors-plugin/webpackModuleError.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { readFileSync } from 'fs'
22
import * as path from 'path'
33
import type { webpack5 as webpack } from 'next/dist/compiled/webpack/webpack'
4-
import type { NextConfig } from '../../../../server/config-shared'
54

65
import { getBabelError } from './parseBabel'
76
import { getCssError } from './parseCss'
@@ -44,8 +43,7 @@ function getFileData(
4443

4544
export async function getModuleBuildError(
4645
compilation: webpack.Compilation,
47-
input: any,
48-
config: NextConfig
46+
input: any
4947
): Promise<SimpleWebpackError | false> {
5048
if (
5149
!(
@@ -65,8 +63,7 @@ export async function getModuleBuildError(
6563
const notFoundError = await getNotFoundError(
6664
compilation,
6765
input,
68-
sourceFilename,
69-
config
66+
sourceFilename
7067
)
7168
if (notFoundError !== false) {
7269
return notFoundError

‎packages/next/server/dev/next-dev-server.ts

+12-9
Original file line numberDiff line numberDiff line change
@@ -795,22 +795,23 @@ export default class DevServer extends Server {
795795
if (isError(err) && err.stack) {
796796
try {
797797
const frames = parseStack(err.stack!)
798-
const frame = frames.find(({ file }) => !file?.startsWith('eval'))!
798+
const frame = frames.find(
799+
({ file }) =>
800+
!file?.startsWith('eval') && !file?.includes('sandbox/context')
801+
)!
799802

800803
if (frame.lineNumber && frame?.file) {
801804
const moduleId = frame.file!.replace(
802805
/^(webpack-internal:\/\/\/|file:\/\/)/,
803806
''
804807
)
805808

806-
let compilation: any
807-
808-
const src = getErrorSource(err)
809-
if (src === 'edge-server') {
810-
compilation = this.hotReloader?.edgeServerStats?.compilation
811-
} else {
812-
compilation = this.hotReloader?.serverStats?.compilation
813-
}
809+
const src = getErrorSource(err as Error)
810+
const compilation = (
811+
src === 'edge-server'
812+
? this.hotReloader?.edgeServerStats?.compilation
813+
: this.hotReloader?.serverStats?.compilation
814+
)!
814815

815816
const source = await getSourceById(
816817
!!frame.file?.startsWith(sep) || !!frame.file?.startsWith('file:'),
@@ -825,6 +826,8 @@ export default class DevServer extends Server {
825826
frame,
826827
modulePath: moduleId,
827828
rootDirectory: this.dir,
829+
errorMessage: err.message,
830+
compilation,
828831
})
829832

830833
if (originalFrame) {

‎packages/next/server/web/sandbox/context.ts

+34-9
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,36 @@ async function createModuleContext(options: ModuleContextOptions) {
129129
return fn()
130130
}
131131

132+
context.__import_unsupported = function __import_unsupported(
133+
moduleName: string
134+
) {
135+
const proxy: any = new Proxy(function () {}, {
136+
get(_obj, prop) {
137+
if (prop === 'then') {
138+
return {}
139+
}
140+
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
141+
},
142+
construct() {
143+
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
144+
},
145+
apply(_target, _this, args) {
146+
if (args[0] instanceof Function) {
147+
return args[0](proxy)
148+
}
149+
throw new Error(getUnsupportedModuleErrorMessage(moduleName))
150+
},
151+
})
152+
return new Proxy({}, { get: () => proxy })
153+
}
154+
132155
context.__next_webassembly_compile__ =
133156
function __next_webassembly_compile__(fn: Function) {
134157
const key = fn.toString()
135158
if (!warnedWasmCodegens.has(key)) {
136159
const warning = getServerError(
137-
new Error(
138-
"Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Middleware.\n" +
139-
'Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation'
140-
),
160+
new Error(`Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Middleware.
161+
Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation`),
141162
'edge-server'
142163
)
143164
warning.name = 'DynamicWasmCodeGenerationWarning'
@@ -163,10 +184,8 @@ async function createModuleContext(options: ModuleContextOptions) {
163184
const key = fn.toString()
164185
if (instantiatedFromBuffer && !warnedWasmCodegens.has(key)) {
165186
const warning = getServerError(
166-
new Error(
167-
"Dynamic WASM code generation ('WebAssembly.instantiate' with a buffer parameter) not allowed in Middleware.\n" +
168-
'Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation'
169-
),
187+
new Error(`Dynamic WASM code generation ('WebAssembly.instantiate' with a buffer parameter) not allowed in Middleware.
188+
Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation`),
170189
'edge-server'
171190
)
172191
warning.name = 'DynamicWasmCodeGenerationWarning'
@@ -321,7 +340,7 @@ function emitWarning(
321340
) {
322341
if (!warnedAlready.has(name)) {
323342
const warning =
324-
new Error(`You're using a Node.js API (${name}) which is not supported in the Edge Runtime that Middleware uses.
343+
new Error(`A Node.js API is used (${name}) which is not supported in the Edge Runtime.
325344
Learn more: https://nextjs.org/docs/api-reference/edge-runtime`)
326345
warning.name = 'NodejsRuntimeApiInMiddlewareWarning'
327346
contextOptions.onWarning(warning)
@@ -335,3 +354,9 @@ function decorateUnhandledError(error: any) {
335354
decorateServerError(error, 'edge-server')
336355
}
337356
}
357+
358+
function getUnsupportedModuleErrorMessage(module: string) {
359+
// warning: if you change these messages, you must adjust how react-dev-overlay's middleware detects modules not found
360+
return `The edge runtime does not support Node.js '${module}' module.
361+
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`
362+
}

‎packages/react-dev-overlay/src/internal/helpers/getErrorByType.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ export async function getErrorByType(
2323
error: event.reason,
2424
frames: await getOriginalStackFrames(
2525
event.frames,
26-
getErrorSource(event.reason)
26+
getErrorSource(event.reason),
27+
event.reason.toString()
2728
),
2829
}
2930
}

‎packages/react-dev-overlay/src/internal/helpers/nodeStackFrames.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export function getServerError(error: Error, type: ErrorType): Error {
3737

3838
n.name = error.name
3939
try {
40-
n.stack = parse(error.stack!)
40+
n.stack = `${n.toString()}\n${parse(error.stack!)
4141
.map(getFilesystemFrame)
4242
.map((f) => {
4343
let str = ` at ${f.methodName}`
@@ -53,7 +53,7 @@ export function getServerError(error: Error, type: ErrorType): Error {
5353
}
5454
return str
5555
})
56-
.join('\n')
56+
.join('\n')}`
5757
} catch {
5858
n.stack = error.stack
5959
}

‎packages/react-dev-overlay/src/internal/helpers/stack-frame.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,24 @@ export type OriginalStackFrame =
3232

3333
export function getOriginalStackFrames(
3434
frames: StackFrame[],
35-
type: 'server' | 'edge-server' | null
35+
type: 'server' | 'edge-server' | null,
36+
errorMessage: string
3637
) {
37-
return Promise.all(frames.map((frame) => getOriginalStackFrame(frame, type)))
38+
return Promise.all(
39+
frames.map((frame) => getOriginalStackFrame(frame, type, errorMessage))
40+
)
3841
}
3942

4043
export function getOriginalStackFrame(
4144
source: StackFrame,
42-
type: 'server' | 'edge-server' | null
45+
type: 'server' | 'edge-server' | null,
46+
errorMessage: string
4347
): Promise<OriginalStackFrame> {
4448
async function _getOriginalStackFrame(): Promise<OriginalStackFrame> {
4549
const params = new URLSearchParams()
4650
params.append('isServer', String(type === 'server'))
4751
params.append('isEdgeServer', String(type === 'edge-server'))
52+
params.append('errorMessage', errorMessage)
4853
for (const key in source) {
4954
params.append(key, ((source as any)[key] ?? '').toString())
5055
}

‎packages/react-dev-overlay/src/middleware.ts

+49-15
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,20 @@ function getModuleId(compilation: any, module: any) {
4040
return compilation.chunkGraph.getModuleId(module)
4141
}
4242

43+
function getModuleById(
44+
id: string | undefined,
45+
compilation: webpack.Compilation
46+
) {
47+
return [...compilation.modules].find(
48+
(searchModule) => getModuleId(compilation, searchModule) === id
49+
)
50+
}
51+
52+
function findModuleNotFoundFromError(errorMessage: string | undefined) {
53+
const match = errorMessage?.match(/'([^']+)' module/)
54+
return match && match[1]
55+
}
56+
4357
function getModuleSource(compilation: any, module: any): any {
4458
return (
4559
(module &&
@@ -100,25 +114,45 @@ async function findOriginalSourcePositionAndContent(
100114
}
101115
}
102116

117+
function findOriginalSourcePositionAndContentFromCompilation(
118+
modulePath: string | undefined,
119+
importedModule: string,
120+
compilation: webpack.Compilation
121+
) {
122+
const module = getModuleById(modulePath, compilation)
123+
return module?.buildInfo?.importLocByPath?.get(importedModule) ?? null
124+
}
125+
103126
export async function createOriginalStackFrame({
104127
line,
105128
column,
106129
source,
107130
modulePath,
108131
rootDirectory,
109132
frame,
133+
errorMessage,
134+
compilation,
110135
}: {
111136
line: number
112137
column: number | null
113138
source: any
114139
modulePath?: string
115140
rootDirectory: string
116141
frame: any
142+
errorMessage?: string
143+
compilation?: webpack.Compilation
117144
}): Promise<OriginalStackFrameResponse | null> {
118-
const result = await findOriginalSourcePositionAndContent(source, {
119-
line,
120-
column,
121-
})
145+
const moduleNotFound = findModuleNotFoundFromError(errorMessage)
146+
const result = moduleNotFound
147+
? findOriginalSourcePositionAndContentFromCompilation(
148+
modulePath,
149+
moduleNotFound,
150+
compilation
151+
)
152+
: await findOriginalSourcePositionAndContent(source, {
153+
line,
154+
column,
155+
})
122156

123157
if (result === null) {
124158
return null
@@ -170,7 +204,7 @@ export async function createOriginalStackFrame({
170204
export async function getSourceById(
171205
isFile: boolean,
172206
id: string,
173-
compilation: any
207+
compilation: webpack.Compilation
174208
): Promise<Source> {
175209
if (isFile) {
176210
const fileContent: string | null = await fs
@@ -198,9 +232,7 @@ export async function getSourceById(
198232
return null
199233
}
200234

201-
const module = [...compilation.modules].find(
202-
(searchModule) => getModuleId(compilation, searchModule) === id
203-
)
235+
const module = getModuleById(id, compilation)
204236
return getModuleSource(compilation, module)
205237
} catch (err) {
206238
console.error(`Failed to lookup module by ID ("${id}"):`, err)
@@ -220,6 +252,7 @@ function getOverlayMiddleware(options: OverlayMiddlewareOptions) {
220252
const frame = query as unknown as StackFrame & {
221253
isEdgeServer: 'true' | 'false'
222254
isServer: 'true' | 'false'
255+
errorMessage: string | undefined
223256
}
224257
if (
225258
!(
@@ -239,14 +272,13 @@ function getOverlayMiddleware(options: OverlayMiddlewareOptions) {
239272
)
240273

241274
let source: Source
275+
const compilation =
276+
frame.isEdgeServer === 'true'
277+
? options.edgeServerStats()?.compilation
278+
: frame.isServer === 'true'
279+
? options.serverStats()?.compilation
280+
: options.stats()?.compilation
242281
try {
243-
const compilation =
244-
frame.isEdgeServer === 'true'
245-
? options.edgeServerStats()?.compilation
246-
: frame.isServer === 'true'
247-
? options.serverStats()?.compilation
248-
: options.stats()?.compilation
249-
250282
source = await getSourceById(
251283
frame.file.startsWith('file:'),
252284
moduleId,
@@ -282,6 +314,8 @@ function getOverlayMiddleware(options: OverlayMiddlewareOptions) {
282314
frame,
283315
modulePath: moduleId,
284316
rootDirectory: options.rootDirectory,
317+
errorMessage: frame.errorMessage,
318+
compilation,
285319
})
286320

287321
if (originalStackFrameResponse === null) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// populated with tests
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// populated with tests
2+
export default () => {}
3+
4+
export const config = { matcher: '/' }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default async function handler(request) {
2+
return Response.json({ ok: true })
3+
}
4+
5+
export const config = { runtime: 'experimental-edge' }

‎test/integration/edge-runtime-module-errors/test/index.test.js

+639
Large diffs are not rendered by default.

‎test/integration/middleware-build-errors/test/index.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { join } from 'path'
55
describe('Middleware validation during build', () => {
66
const appDir = join(__dirname, '..')
77
const middlewareFile = join(appDir, 'middleware.js')
8-
const middlewareError = 'Your middleware is returning a response body'
8+
const middlewareError = 'Middleware is returning a response body'
99

1010
beforeEach(() => remove(join(appDir, '.next')))
1111

‎test/integration/middleware-module-errors/middleware.js

-1
This file was deleted.

‎test/integration/middleware-module-errors/test/index.test.js

-256
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { NextResponse } from 'next/server'
2+
3+
export default function middleware() {
4+
return NextResponse.next()
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export function middleware() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export function middleware() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <div>ok</div>
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/* eslint-env jest */
2+
3+
import { join } from 'path'
4+
import {
5+
fetchViaHTTP,
6+
findPort,
7+
killApp,
8+
launchApp,
9+
nextBuild,
10+
} from 'next-test-utils'
11+
12+
jest.setTimeout(1000 * 60 * 2)
13+
14+
const context = {
15+
appDir: join(__dirname, '../'),
16+
logs: { output: '', stdout: '', stderr: '' },
17+
}
18+
19+
describe('Middleware importing Node.js modules', () => {
20+
afterEach(() => {
21+
if (context.app) {
22+
killApp(context.app)
23+
}
24+
})
25+
26+
describe('dev mode', () => {
27+
// restart the app for every test since the latest error is not shown sometimes
28+
// See https://github.com/vercel/next.js/issues/36575
29+
beforeEach(async () => {
30+
context.logs = { stdout: '', stderr: '' }
31+
context.appPort = await findPort()
32+
context.app = await launchApp(context.appDir, context.appPort, {
33+
env: { __NEXT_TEST_WITH_DEVTOOL: 1 },
34+
onStdout(msg) {
35+
context.logs.stdout += msg
36+
},
37+
onStderr(msg) {
38+
context.logs.stderr += msg
39+
},
40+
})
41+
})
42+
43+
it('warns about nested middleware being not allowed', async () => {
44+
const res = await fetchViaHTTP(context.appPort, '/about')
45+
expect(res.status).toBe(200)
46+
context.logs.stderr.includes('Nested Middleware is not allowed, found:')
47+
context.logs.stderr.includes('pages/about/_middleware')
48+
context.logs.stderr.includes('pages/api/_middleware')
49+
})
50+
})
51+
52+
describe('production mode', () => {
53+
it('fails when there is a not allowed middleware', async () => {
54+
const buildResult = await nextBuild(context.appDir, undefined, {
55+
stderr: true,
56+
stdout: true,
57+
})
58+
expect(buildResult.stderr).toContain(
59+
'Nested Middleware is not allowed, found:'
60+
)
61+
expect(buildResult.stderr).toContain('pages/about/_middleware')
62+
expect(buildResult.stderr).toContain('pages/api/_middleware')
63+
})
64+
})
65+
})

‎test/integration/middleware-overrides-node.js-api/test/index.test.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,10 @@ describe('Middleware overriding a Node.js API', () => {
3636
const res = await fetchViaHTTP(context.appPort, '/')
3737
await waitFor(500)
3838
expect(res.status).toBe(200)
39-
expect(output)
40-
.toContain(`NodejsRuntimeApiInMiddlewareWarning: You're using a Node.js API (process.cwd) which is not supported in the Edge Runtime that Middleware uses.
41-
Learn more: https://nextjs.org/docs/api-reference/edge-runtime`)
39+
expect(output).toContain('A Node.js API is used (process.cwd')
4240
expect(output).toContain('fixed-value')
4341
expect(output).not.toContain('TypeError')
44-
expect(output).not.toContain(`You're using a Node.js API (process.env)`)
42+
expect(output).not.toContain(`A Node.js API is used (process.env`)
4543
})
4644
})
4745
})

‎test/integration/middleware-with-node.js-apis/test/index.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ describe('Middleware using Node.js API', () => {
8585
expect(res.status).toBe(500)
8686
await check(
8787
() =>
88-
output.includes(`NodejsRuntimeApiInMiddlewareWarning: You're using a Node.js API (${api}) which is not supported in the Edge Runtime that Middleware uses.
88+
output.includes(`A Node.js API is used (${api}) which is not supported in the Edge Runtime.
8989
Learn more: https://nextjs.org/docs/api-reference/edge-runtime`)
9090
? 'success'
9191
: output,
@@ -118,7 +118,7 @@ Learn more: https://nextjs.org/docs/api-reference/edge-runtime`)
118118
)
119119
)(`warns for $api during build`, ({ api, line }) => {
120120
expect(buildResult.stderr)
121-
.toContain(`You're using a Node.js API (${api} at line: ${line}) which is not supported in the Edge Runtime that Middleware uses.
121+
.toContain(`A Node.js API is used (${api} at line: ${line}) which is not supported in the Edge Runtime.
122122
Learn more: https://nextjs.org/docs/api-reference/edge-runtime`)
123123
})
124124
})

0 commit comments

Comments
 (0)
Please sign in to comment.