2022-07-19 19:27:15 +02:00
|
|
|
import type {
|
|
|
|
AssetBinding,
|
|
|
|
EdgeMiddlewareMeta,
|
|
|
|
} from '../loaders/get-module-build-info'
|
|
|
|
import type { EdgeSSRMeta } from '../loaders/get-module-build-info'
|
2022-05-19 17:46:21 +02:00
|
|
|
import { getNamedMiddlewareRegex } from '../../../shared/lib/router/utils/route-regex'
|
2022-04-30 13:19:27 +02:00
|
|
|
import { getModuleBuildInfo } from '../loaders/get-module-build-info'
|
2021-10-20 19:52:11 +02:00
|
|
|
import { getSortedRoutes } from '../../../shared/lib/router/utils'
|
2022-08-16 11:55:37 +02:00
|
|
|
import { webpack, sources } from 'next/dist/compiled/webpack/webpack'
|
2021-10-26 18:50:56 +02:00
|
|
|
import {
|
2022-04-30 13:19:27 +02:00
|
|
|
EDGE_RUNTIME_WEBPACK,
|
2022-05-23 11:07:26 +02:00
|
|
|
EDGE_UNSUPPORTED_NODE_APIS,
|
2021-10-26 18:50:56 +02:00
|
|
|
MIDDLEWARE_BUILD_MANIFEST,
|
2022-06-26 23:01:26 +02:00
|
|
|
FLIGHT_MANIFEST,
|
2022-04-30 13:19:27 +02:00
|
|
|
MIDDLEWARE_MANIFEST,
|
2021-10-26 18:50:56 +02:00
|
|
|
MIDDLEWARE_REACT_LOADABLE_MANIFEST,
|
2022-05-18 13:18:28 +02:00
|
|
|
NEXT_CLIENT_SSR_ENTRY_SUFFIX,
|
2021-10-26 18:50:56 +02:00
|
|
|
} from '../../../shared/lib/constants'
|
|
|
|
|
2022-07-19 19:27:15 +02:00
|
|
|
export interface EdgeFunctionDefinition {
|
2022-06-13 20:17:44 +02:00
|
|
|
env: string[]
|
|
|
|
files: string[]
|
|
|
|
name: string
|
|
|
|
page: string
|
|
|
|
regexp: string
|
2022-07-19 19:27:15 +02:00
|
|
|
wasm?: AssetBinding[]
|
|
|
|
assets?: AssetBinding[]
|
2022-06-13 20:17:44 +02:00
|
|
|
}
|
|
|
|
|
2021-10-20 19:52:11 +02:00
|
|
|
export interface MiddlewareManifest {
|
|
|
|
version: 1
|
2021-10-26 22:18:08 +02:00
|
|
|
sortedMiddleware: string[]
|
2022-06-13 20:17:44 +02:00
|
|
|
middleware: { [page: string]: EdgeFunctionDefinition }
|
|
|
|
functions: { [page: string]: EdgeFunctionDefinition }
|
2021-10-20 19:52:11 +02:00
|
|
|
}
|
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
interface EntryMetadata {
|
|
|
|
edgeMiddleware?: EdgeMiddlewareMeta
|
2022-06-13 20:17:44 +02:00
|
|
|
edgeApiFunction?: EdgeMiddlewareMeta
|
2022-04-30 13:19:27 +02:00
|
|
|
edgeSSR?: EdgeSSRMeta
|
|
|
|
env: Set<string>
|
2022-07-19 19:27:15 +02:00
|
|
|
wasmBindings: Map<string, string>
|
|
|
|
assetBindings: Map<string, string>
|
2022-04-30 13:19:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const NAME = 'MiddlewarePlugin'
|
2021-11-08 13:41:49 +01:00
|
|
|
const middlewareManifest: MiddlewareManifest = {
|
|
|
|
sortedMiddleware: [],
|
|
|
|
middleware: {},
|
2022-06-13 20:17:44 +02:00
|
|
|
functions: {},
|
2021-11-08 13:41:49 +01:00
|
|
|
version: 1,
|
|
|
|
}
|
2022-01-31 16:46:04 +01:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
/**
|
|
|
|
* Checks the value of usingIndirectEval and when it is a set of modules it
|
|
|
|
* check if any of the modules is actually being used. If the value is
|
|
|
|
* simply truthy it will return true.
|
|
|
|
*/
|
|
|
|
function isUsingIndirectEvalAndUsedByExports(args: {
|
2022-08-16 11:55:37 +02:00
|
|
|
entryModule: webpack.Module
|
|
|
|
moduleGraph: webpack.ModuleGraph
|
2022-08-15 16:29:51 +02:00
|
|
|
runtime: any
|
|
|
|
usingIndirectEval: true | Set<string>
|
2022-08-16 11:55:37 +02:00
|
|
|
wp: typeof webpack
|
2022-08-15 16:29:51 +02:00
|
|
|
}): boolean {
|
|
|
|
const { moduleGraph, runtime, entryModule, usingIndirectEval, wp } = args
|
|
|
|
if (typeof usingIndirectEval === 'boolean') {
|
|
|
|
return usingIndirectEval
|
|
|
|
}
|
2022-03-02 16:09:36 +01:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
const exportsInfo = moduleGraph.getExportsInfo(entryModule)
|
|
|
|
for (const exportName of usingIndirectEval) {
|
|
|
|
if (exportsInfo.getUsed(exportName, runtime) !== wp.UsageState.Unused) {
|
|
|
|
return true
|
|
|
|
}
|
2022-04-30 13:19:27 +02:00
|
|
|
}
|
2022-01-31 16:46:04 +01:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
return false
|
|
|
|
}
|
2022-04-30 13:19:27 +02:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
function getEntryFiles(entryFiles: string[], meta: EntryMetadata) {
|
|
|
|
const files: string[] = []
|
|
|
|
if (meta.edgeSSR) {
|
|
|
|
if (meta.edgeSSR.isServerComponent) {
|
|
|
|
files.push(`server/${FLIGHT_MANIFEST}.js`)
|
|
|
|
files.push(
|
|
|
|
...entryFiles
|
|
|
|
.filter(
|
|
|
|
(file) =>
|
|
|
|
file.startsWith('pages/') && !file.endsWith('.hot-update.js')
|
|
|
|
)
|
|
|
|
.map(
|
|
|
|
(file) =>
|
|
|
|
'server/' +
|
|
|
|
// TODO-APP: seems this should be removed.
|
|
|
|
file.replace('.js', NEXT_CLIENT_SSR_ENTRY_SUFFIX + '.js')
|
|
|
|
)
|
2022-04-30 13:19:27 +02:00
|
|
|
)
|
2022-08-15 16:29:51 +02:00
|
|
|
}
|
2022-01-31 16:46:04 +01:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
files.push(
|
|
|
|
`server/${MIDDLEWARE_BUILD_MANIFEST}.js`,
|
|
|
|
`server/${MIDDLEWARE_REACT_LOADABLE_MANIFEST}.js`
|
|
|
|
)
|
2022-01-31 16:46:04 +01:00
|
|
|
}
|
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
files.push(
|
|
|
|
...entryFiles
|
|
|
|
.filter((file) => !file.endsWith('.hot-update.js'))
|
|
|
|
.map((file) => 'server/' + file)
|
|
|
|
)
|
|
|
|
return files
|
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`)
2022-07-06 22:54:44 +02:00
|
|
|
}
|
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
function getCreateAssets(params: {
|
2022-08-16 11:55:37 +02:00
|
|
|
compilation: webpack.Compilation
|
2022-08-15 16:29:51 +02:00
|
|
|
metadataByEntry: Map<string, EntryMetadata>
|
2022-04-30 13:19:27 +02:00
|
|
|
}) {
|
2022-08-15 16:29:51 +02:00
|
|
|
const { compilation, metadataByEntry } = params
|
|
|
|
return (assets: any) => {
|
|
|
|
for (const entrypoint of compilation.entrypoints.values()) {
|
|
|
|
if (!entrypoint.name) {
|
|
|
|
continue
|
2022-08-11 23:32:52 +02:00
|
|
|
}
|
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
// There should always be metadata for the entrypoint.
|
|
|
|
const metadata = metadataByEntry.get(entrypoint.name)
|
|
|
|
const page =
|
|
|
|
metadata?.edgeMiddleware?.page ||
|
|
|
|
metadata?.edgeSSR?.page ||
|
|
|
|
metadata?.edgeApiFunction?.page
|
|
|
|
if (!page) {
|
|
|
|
continue
|
|
|
|
}
|
2022-08-11 23:32:52 +02:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
const { namedRegex } = getNamedMiddlewareRegex(page, {
|
|
|
|
catchAll: !metadata.edgeSSR && !metadata.edgeApiFunction,
|
2022-08-11 23:32:52 +02:00
|
|
|
})
|
2022-08-15 16:29:51 +02:00
|
|
|
const regexp = metadata?.edgeMiddleware?.matcherRegexp || namedRegex
|
2022-08-11 23:32:52 +02:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
const edgeFunctionDefinition: EdgeFunctionDefinition = {
|
|
|
|
env: Array.from(metadata.env),
|
|
|
|
files: getEntryFiles(entrypoint.getFiles(), metadata),
|
|
|
|
name: entrypoint.name,
|
|
|
|
page: page,
|
|
|
|
regexp,
|
|
|
|
wasm: Array.from(metadata.wasmBindings, ([name, filePath]) => ({
|
|
|
|
name,
|
|
|
|
filePath,
|
|
|
|
})),
|
|
|
|
assets: Array.from(metadata.assetBindings, ([name, filePath]) => ({
|
|
|
|
name,
|
|
|
|
filePath,
|
|
|
|
})),
|
2022-04-30 13:19:27 +02:00
|
|
|
}
|
2022-04-27 11:50:29 +02:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
if (metadata.edgeApiFunction || metadata.edgeSSR) {
|
|
|
|
middlewareManifest.functions[page] = edgeFunctionDefinition
|
|
|
|
} else {
|
|
|
|
middlewareManifest.middleware[page] = edgeFunctionDefinition
|
2022-04-30 13:19:27 +02:00
|
|
|
}
|
|
|
|
}
|
2021-10-20 19:52:11 +02:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
middlewareManifest.sortedMiddleware = getSortedRoutes(
|
|
|
|
Object.keys(middlewareManifest.middleware)
|
|
|
|
)
|
2022-06-16 16:59:30 +02:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
assets[MIDDLEWARE_MANIFEST] = new sources.RawSource(
|
|
|
|
JSON.stringify(middlewareManifest, null, 2)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2022-06-16 16:59:30 +02:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
function buildWebpackError({
|
|
|
|
message,
|
|
|
|
loc,
|
|
|
|
compilation,
|
|
|
|
entryModule,
|
|
|
|
parser,
|
|
|
|
}: {
|
|
|
|
message: string
|
|
|
|
loc?: any
|
2022-08-16 11:55:37 +02:00
|
|
|
compilation: webpack.Compilation
|
|
|
|
entryModule?: webpack.Module
|
|
|
|
parser?: webpack.javascript.JavascriptParser
|
2022-08-15 16:29:51 +02:00
|
|
|
}) {
|
|
|
|
const error = new compilation.compiler.webpack.WebpackError(message)
|
|
|
|
error.name = NAME
|
|
|
|
const module = entryModule ?? parser?.state.current
|
|
|
|
if (module) {
|
|
|
|
error.module = module
|
|
|
|
}
|
|
|
|
error.loc = loc
|
|
|
|
return error
|
|
|
|
}
|
|
|
|
|
2022-08-16 11:55:37 +02:00
|
|
|
function isInMiddlewareLayer(parser: webpack.javascript.JavascriptParser) {
|
2022-08-15 16:29:51 +02:00
|
|
|
return parser.state.module?.layer === 'middleware'
|
|
|
|
}
|
|
|
|
|
2022-08-16 11:55:37 +02:00
|
|
|
function isInMiddlewareFile(parser: webpack.javascript.JavascriptParser) {
|
2022-08-15 16:29:51 +02:00
|
|
|
return (
|
|
|
|
parser.state.current?.layer === 'middleware' &&
|
|
|
|
/middleware\.\w+$/.test(parser.state.current?.rawRequest)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
function isNullLiteral(expr: any) {
|
|
|
|
return expr.value === null
|
|
|
|
}
|
|
|
|
|
|
|
|
function isUndefinedIdentifier(expr: any) {
|
|
|
|
return expr.name === 'undefined'
|
|
|
|
}
|
|
|
|
|
|
|
|
function isProcessEnvMemberExpression(memberExpression: any): boolean {
|
|
|
|
return (
|
|
|
|
memberExpression.object?.type === 'Identifier' &&
|
|
|
|
memberExpression.object.name === 'process' &&
|
|
|
|
((memberExpression.property?.type === 'Literal' &&
|
|
|
|
memberExpression.property.value === 'env') ||
|
|
|
|
(memberExpression.property?.type === 'Identifier' &&
|
|
|
|
memberExpression.property.name === 'env'))
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
function isNodeJsModule(moduleName: string) {
|
|
|
|
return require('module').builtinModules.includes(moduleName)
|
|
|
|
}
|
|
|
|
|
|
|
|
function buildUnsupportedApiError({
|
|
|
|
apiName,
|
|
|
|
loc,
|
|
|
|
...rest
|
|
|
|
}: {
|
|
|
|
apiName: string
|
|
|
|
loc: any
|
2022-08-16 11:55:37 +02:00
|
|
|
compilation: webpack.Compilation
|
|
|
|
parser: webpack.javascript.JavascriptParser
|
2022-08-15 16:29:51 +02:00
|
|
|
}) {
|
|
|
|
return buildWebpackError({
|
|
|
|
message: `A Node.js API is used (${apiName} at line: ${loc.start.line}) which is not supported in the Edge Runtime.
|
|
|
|
Learn more: https://nextjs.org/docs/api-reference/edge-runtime`,
|
|
|
|
loc,
|
|
|
|
...rest,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
function registerUnsupportedApiHooks(
|
2022-08-16 11:55:37 +02:00
|
|
|
parser: webpack.javascript.JavascriptParser,
|
|
|
|
compilation: webpack.Compilation
|
2022-08-15 16:29:51 +02:00
|
|
|
) {
|
|
|
|
for (const expression of EDGE_UNSUPPORTED_NODE_APIS) {
|
|
|
|
const warnForUnsupportedApi = (node: any) => {
|
|
|
|
if (!isInMiddlewareLayer(parser)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
compilation.warnings.push(
|
|
|
|
buildUnsupportedApiError({
|
|
|
|
compilation,
|
|
|
|
parser,
|
|
|
|
apiName: expression,
|
|
|
|
...node,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
parser.hooks.call.for(expression).tap(NAME, warnForUnsupportedApi)
|
|
|
|
parser.hooks.expression.for(expression).tap(NAME, warnForUnsupportedApi)
|
|
|
|
parser.hooks.callMemberChain
|
|
|
|
.for(expression)
|
|
|
|
.tap(NAME, warnForUnsupportedApi)
|
|
|
|
parser.hooks.expressionMemberChain
|
|
|
|
.for(expression)
|
|
|
|
.tap(NAME, warnForUnsupportedApi)
|
|
|
|
}
|
|
|
|
|
|
|
|
const warnForUnsupportedProcessApi = (node: any, [callee]: string[]) => {
|
|
|
|
if (!isInMiddlewareLayer(parser) || callee === 'env') {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
compilation.warnings.push(
|
|
|
|
buildUnsupportedApiError({
|
|
|
|
compilation,
|
|
|
|
parser,
|
|
|
|
apiName: `process.${callee}`,
|
|
|
|
...node,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
parser.hooks.callMemberChain
|
|
|
|
.for('process')
|
|
|
|
.tap(NAME, warnForUnsupportedProcessApi)
|
|
|
|
parser.hooks.expressionMemberChain
|
|
|
|
.for('process')
|
|
|
|
.tap(NAME, warnForUnsupportedProcessApi)
|
|
|
|
}
|
|
|
|
|
|
|
|
function getCodeAnalyzer(params: {
|
|
|
|
dev: boolean
|
2022-08-16 11:55:37 +02:00
|
|
|
compiler: webpack.Compiler
|
|
|
|
compilation: webpack.Compilation
|
2022-08-15 16:29:51 +02:00
|
|
|
}) {
|
2022-08-16 11:55:37 +02:00
|
|
|
return (parser: webpack.javascript.JavascriptParser) => {
|
2022-08-15 16:29:51 +02:00
|
|
|
const {
|
|
|
|
dev,
|
|
|
|
compiler: { webpack: wp },
|
|
|
|
compilation,
|
|
|
|
} = params
|
|
|
|
const { hooks } = parser
|
|
|
|
|
|
|
|
/**
|
|
|
|
* For an expression this will check the graph to ensure it is being used
|
|
|
|
* by exports. Then it will store in the module buildInfo a boolean to
|
|
|
|
* express that it contains dynamic code and, if it is available, the
|
|
|
|
* module path that is using it.
|
|
|
|
*/
|
|
|
|
const handleExpression = () => {
|
|
|
|
if (!isInMiddlewareLayer(parser)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
wp.optimize.InnerGraph.onUsage(parser.state, (used = true) => {
|
|
|
|
const buildInfo = getModuleBuildInfo(parser.state.module)
|
|
|
|
if (buildInfo.usingIndirectEval === true || used === false) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!buildInfo.usingIndirectEval || used === true) {
|
|
|
|
buildInfo.usingIndirectEval = used
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
buildInfo.usingIndirectEval = new Set([
|
|
|
|
...Array.from(buildInfo.usingIndirectEval),
|
|
|
|
...Array.from(used),
|
|
|
|
])
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This expression handler allows to wrap a dynamic code expression with a
|
|
|
|
* function call where we can warn about dynamic code not being allowed
|
|
|
|
* but actually execute the expression.
|
|
|
|
*/
|
|
|
|
const handleWrapExpression = (expr: any) => {
|
|
|
|
if (!isInMiddlewareLayer(parser)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dev) {
|
|
|
|
const { ConstDependency } = wp.dependencies
|
|
|
|
const dep1 = new ConstDependency(
|
|
|
|
'__next_eval__(function() { return ',
|
|
|
|
expr.range[0]
|
|
|
|
)
|
|
|
|
dep1.loc = expr.loc
|
|
|
|
parser.state.module.addPresentationalDependency(dep1)
|
|
|
|
const dep2 = new ConstDependency('})', expr.range[1])
|
|
|
|
dep2.loc = expr.loc
|
|
|
|
parser.state.module.addPresentationalDependency(dep2)
|
|
|
|
}
|
|
|
|
|
|
|
|
handleExpression()
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This expression handler allows to wrap a WebAssembly.compile invocation with a
|
|
|
|
* function call where we can warn about WASM code generation not being allowed
|
|
|
|
* but actually execute the expression.
|
|
|
|
*/
|
|
|
|
const handleWrapWasmCompileExpression = (expr: any) => {
|
|
|
|
if (!isInMiddlewareLayer(parser)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dev) {
|
|
|
|
const { ConstDependency } = wp.dependencies
|
|
|
|
const dep1 = new ConstDependency(
|
|
|
|
'__next_webassembly_compile__(function() { return ',
|
|
|
|
expr.range[0]
|
|
|
|
)
|
|
|
|
dep1.loc = expr.loc
|
|
|
|
parser.state.module.addPresentationalDependency(dep1)
|
|
|
|
const dep2 = new ConstDependency('})', expr.range[1])
|
|
|
|
dep2.loc = expr.loc
|
|
|
|
parser.state.module.addPresentationalDependency(dep2)
|
|
|
|
}
|
|
|
|
|
|
|
|
handleExpression()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This expression handler allows to wrap a WebAssembly.instatiate invocation with a
|
|
|
|
* function call where we can warn about WASM code generation not being allowed
|
|
|
|
* but actually execute the expression.
|
|
|
|
*
|
|
|
|
* Note that we don't update `usingIndirectEval`, i.e. we don't abort a production build
|
|
|
|
* since we can't determine statically if the first parameter is a module (legit use) or
|
|
|
|
* a buffer (dynamic code generation).
|
|
|
|
*/
|
|
|
|
const handleWrapWasmInstantiateExpression = (expr: any) => {
|
|
|
|
if (!isInMiddlewareLayer(parser)) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if (dev) {
|
|
|
|
const { ConstDependency } = wp.dependencies
|
|
|
|
const dep1 = new ConstDependency(
|
|
|
|
'__next_webassembly_instantiate__(function() { return ',
|
|
|
|
expr.range[0]
|
|
|
|
)
|
2022-06-16 16:59:30 +02:00
|
|
|
dep1.loc = expr.loc
|
|
|
|
parser.state.module.addPresentationalDependency(dep1)
|
|
|
|
const dep2 = new ConstDependency('})', expr.range[1])
|
|
|
|
dep2.loc = expr.loc
|
|
|
|
parser.state.module.addPresentationalDependency(dep2)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
/**
|
2022-06-09 13:49:58 +02:00
|
|
|
* Declares an environment variable that is being used in this module
|
|
|
|
* through this static analysis.
|
|
|
|
*/
|
|
|
|
const addUsedEnvVar = (envVarName: string) => {
|
|
|
|
const buildInfo = getModuleBuildInfo(parser.state.module)
|
|
|
|
if (buildInfo.nextUsedEnvVars === undefined) {
|
|
|
|
buildInfo.nextUsedEnvVars = new Set()
|
|
|
|
}
|
|
|
|
|
|
|
|
buildInfo.nextUsedEnvVars.add(envVarName)
|
|
|
|
}
|
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
/**
|
|
|
|
* A handler for calls to `process.env` where we identify the name of the
|
|
|
|
* ENV variable being assigned and store it in the module info.
|
|
|
|
*/
|
|
|
|
const handleCallMemberChain = (_: unknown, members: string[]) => {
|
|
|
|
if (members.length >= 2 && members[0] === 'env') {
|
2022-06-09 13:49:58 +02:00
|
|
|
addUsedEnvVar(members[1])
|
2022-05-23 11:07:26 +02:00
|
|
|
if (!isInMiddlewareLayer(parser)) {
|
2022-01-31 16:46:04 +01:00
|
|
|
return true
|
|
|
|
}
|
2022-04-30 13:19:27 +02:00
|
|
|
}
|
|
|
|
}
|
2021-11-05 21:48:43 +01:00
|
|
|
|
feat(middleware)!: forbids middleware response body (#36835)
_Hello Next.js team! First PR here, I hope I've followed the right practices._
### What's in there?
It has been decided to only support the following uses cases in Next.js' middleware:
- rewrite the URL (`x-middleware-rewrite` response header)
- redirect to another URL (`Location` response header)
- pass on to the next piece in the request pipeline (`x-middleware-next` response header)
1. during development, a warning on console tells developers when they are returning a response (either with `Response` or `NextResponse`).
2. at build time, this warning becomes an error.
3. at run time, returning a response body will trigger a 500 HTTP error with a JSON payload containing the detailed error.
All returned/thrown errors contain a link to the documentation.
This is a breaking feature compared to the _beta_ middleware implementation, and also removes `NextResponse.json()` which makes no sense any more.
### How to try it?
- runtime behavior: `HEADLESS=true yarn jest test/integration/middleware/core`
- build behavior : `yarn jest test/integration/middleware/build-errors`
- development behavior: `HEADLESS=true yarn jest test/development/middleware-warnings`
### Notes to reviewers
The limitation happens in next's web adapter. ~The initial implementation was to check `response.body` existence, but it turns out [`Response.redirect()`](https://github.com/vercel/next.js/blob/canary/packages/next/server/web/spec-compliant/response.ts#L42-L53) may set the response body (https://github.com/vercel/next.js/pull/31886). Hence why the proposed implementation specifically looks at response headers.~
`Response.redirect()` and `NextResponse.redirect()` do not need to include the final location in their body: it is handled by next server https://github.com/vercel/next.js/blob/canary/packages/next/server/next-server.ts#L1142
Because this is a breaking change, I had to adjust several tests cases, previously returning JSON/stream/text bodies. When relevant, these middlewares are returning data using response headers.
About DevEx: relying on AST analysis to detect forbidden use cases is not as good as running the code.
Such cases are easy to detect:
```js
new Response('a text value')
new Response(JSON.stringify({ /* whatever */ })
```
But these are false-positive cases:
```js
function returnNull() { return null }
new Response(returnNull())
function doesNothing() {}
new Response(doesNothing())
```
However, I see no good reasons to let users ship middleware such as the one above, hence why the build will fail, even if _technically speaking_, they are not setting the response body.
## Feature
- [x] 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`
- [x] Integration tests added
- [x] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [x] Errors have helpful link attached, see `contributing.md`
## Documentation / Examples
- [x] Make sure the linting passes by running `yarn lint`
2022-05-20 00:02:20 +02:00
|
|
|
/**
|
|
|
|
* A handler for calls to `new Response()` so we can fail if user is setting the response's body.
|
|
|
|
*/
|
|
|
|
const handleNewResponseExpression = (node: any) => {
|
|
|
|
const firstParameter = node?.arguments?.[0]
|
|
|
|
if (
|
2022-05-23 11:07:26 +02:00
|
|
|
isInMiddlewareFile(parser) &&
|
feat(middleware)!: forbids middleware response body (#36835)
_Hello Next.js team! First PR here, I hope I've followed the right practices._
### What's in there?
It has been decided to only support the following uses cases in Next.js' middleware:
- rewrite the URL (`x-middleware-rewrite` response header)
- redirect to another URL (`Location` response header)
- pass on to the next piece in the request pipeline (`x-middleware-next` response header)
1. during development, a warning on console tells developers when they are returning a response (either with `Response` or `NextResponse`).
2. at build time, this warning becomes an error.
3. at run time, returning a response body will trigger a 500 HTTP error with a JSON payload containing the detailed error.
All returned/thrown errors contain a link to the documentation.
This is a breaking feature compared to the _beta_ middleware implementation, and also removes `NextResponse.json()` which makes no sense any more.
### How to try it?
- runtime behavior: `HEADLESS=true yarn jest test/integration/middleware/core`
- build behavior : `yarn jest test/integration/middleware/build-errors`
- development behavior: `HEADLESS=true yarn jest test/development/middleware-warnings`
### Notes to reviewers
The limitation happens in next's web adapter. ~The initial implementation was to check `response.body` existence, but it turns out [`Response.redirect()`](https://github.com/vercel/next.js/blob/canary/packages/next/server/web/spec-compliant/response.ts#L42-L53) may set the response body (https://github.com/vercel/next.js/pull/31886). Hence why the proposed implementation specifically looks at response headers.~
`Response.redirect()` and `NextResponse.redirect()` do not need to include the final location in their body: it is handled by next server https://github.com/vercel/next.js/blob/canary/packages/next/server/next-server.ts#L1142
Because this is a breaking change, I had to adjust several tests cases, previously returning JSON/stream/text bodies. When relevant, these middlewares are returning data using response headers.
About DevEx: relying on AST analysis to detect forbidden use cases is not as good as running the code.
Such cases are easy to detect:
```js
new Response('a text value')
new Response(JSON.stringify({ /* whatever */ })
```
But these are false-positive cases:
```js
function returnNull() { return null }
new Response(returnNull())
function doesNothing() {}
new Response(doesNothing())
```
However, I see no good reasons to let users ship middleware such as the one above, hence why the build will fail, even if _technically speaking_, they are not setting the response body.
## Feature
- [x] 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`
- [x] Integration tests added
- [x] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [x] Errors have helpful link attached, see `contributing.md`
## Documentation / Examples
- [x] Make sure the linting passes by running `yarn lint`
2022-05-20 00:02:20 +02:00
|
|
|
firstParameter &&
|
|
|
|
!isNullLiteral(firstParameter) &&
|
|
|
|
!isUndefinedIdentifier(firstParameter)
|
|
|
|
) {
|
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`)
2022-07-06 22:54:44 +02:00
|
|
|
const error = buildWebpackError({
|
|
|
|
message: `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`,
|
|
|
|
compilation,
|
|
|
|
parser,
|
|
|
|
...node,
|
|
|
|
})
|
feat(middleware)!: forbids middleware response body (#36835)
_Hello Next.js team! First PR here, I hope I've followed the right practices._
### What's in there?
It has been decided to only support the following uses cases in Next.js' middleware:
- rewrite the URL (`x-middleware-rewrite` response header)
- redirect to another URL (`Location` response header)
- pass on to the next piece in the request pipeline (`x-middleware-next` response header)
1. during development, a warning on console tells developers when they are returning a response (either with `Response` or `NextResponse`).
2. at build time, this warning becomes an error.
3. at run time, returning a response body will trigger a 500 HTTP error with a JSON payload containing the detailed error.
All returned/thrown errors contain a link to the documentation.
This is a breaking feature compared to the _beta_ middleware implementation, and also removes `NextResponse.json()` which makes no sense any more.
### How to try it?
- runtime behavior: `HEADLESS=true yarn jest test/integration/middleware/core`
- build behavior : `yarn jest test/integration/middleware/build-errors`
- development behavior: `HEADLESS=true yarn jest test/development/middleware-warnings`
### Notes to reviewers
The limitation happens in next's web adapter. ~The initial implementation was to check `response.body` existence, but it turns out [`Response.redirect()`](https://github.com/vercel/next.js/blob/canary/packages/next/server/web/spec-compliant/response.ts#L42-L53) may set the response body (https://github.com/vercel/next.js/pull/31886). Hence why the proposed implementation specifically looks at response headers.~
`Response.redirect()` and `NextResponse.redirect()` do not need to include the final location in their body: it is handled by next server https://github.com/vercel/next.js/blob/canary/packages/next/server/next-server.ts#L1142
Because this is a breaking change, I had to adjust several tests cases, previously returning JSON/stream/text bodies. When relevant, these middlewares are returning data using response headers.
About DevEx: relying on AST analysis to detect forbidden use cases is not as good as running the code.
Such cases are easy to detect:
```js
new Response('a text value')
new Response(JSON.stringify({ /* whatever */ })
```
But these are false-positive cases:
```js
function returnNull() { return null }
new Response(returnNull())
function doesNothing() {}
new Response(doesNothing())
```
However, I see no good reasons to let users ship middleware such as the one above, hence why the build will fail, even if _technically speaking_, they are not setting the response body.
## Feature
- [x] 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`
- [x] Integration tests added
- [x] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [x] Errors have helpful link attached, see `contributing.md`
## Documentation / Examples
- [x] Make sure the linting passes by running `yarn lint`
2022-05-20 00:02:20 +02:00
|
|
|
if (dev) {
|
|
|
|
compilation.warnings.push(error)
|
|
|
|
} else {
|
|
|
|
compilation.errors.push(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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`)
2022-07-06 22:54:44 +02:00
|
|
|
/**
|
|
|
|
* Handler to store original source location of static and dynamic imports into module's buildInfo.
|
|
|
|
*/
|
|
|
|
const handleImport = (node: any) => {
|
|
|
|
if (isInMiddlewareLayer(parser) && node.source?.value && node?.loc) {
|
|
|
|
const { module, source } = parser.state
|
|
|
|
const buildInfo = getModuleBuildInfo(module)
|
|
|
|
if (!buildInfo.importLocByPath) {
|
|
|
|
buildInfo.importLocByPath = new Map()
|
|
|
|
}
|
|
|
|
|
|
|
|
const importedModule = node.source.value?.toString()!
|
|
|
|
buildInfo.importLocByPath.set(importedModule, {
|
|
|
|
sourcePosition: {
|
|
|
|
...node.loc.start,
|
|
|
|
source: module.identifier(),
|
|
|
|
},
|
|
|
|
sourceContent: source.toString(),
|
|
|
|
})
|
|
|
|
|
|
|
|
if (!dev && isNodeJsModule(importedModule)) {
|
|
|
|
compilation.warnings.push(
|
|
|
|
buildWebpackError({
|
|
|
|
message: `A Node.js module is loaded ('${importedModule}' at line ${node.loc.start.line}) which is not supported in the Edge Runtime.
|
|
|
|
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`,
|
|
|
|
compilation,
|
|
|
|
parser,
|
|
|
|
...node,
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
/**
|
|
|
|
* A noop handler to skip analyzing some cases.
|
fix(middleware): false positive dynamic code detection at build time (#36955)
## What's in there?
Partially fixes https://github.com/vercel/edge-functions/issues/82
Relates to #36715
Our webpack plugin for middleware leverages static analysis to detect Dyanamic code evaluation in user `_middleware.js` file (and depedencies). Since edge function runtime do not allow them, the build is aborted.
The use of `Function.bind` is considered invalid, while it is legit. A customer using `@aws-sdk/client-s3` reported it.
This PR fixes it.
Please note that this check is too strict: some dynamic code may be in the bundle (despite treeshaking), but may never be used (because of code branches). Since this point is under discussion, this PR adds tests covering some false positives (`@apollo/react-hook`, `qs` and `has`), but does not change the behavior (consider them as errors).
## Notes to reviewer
I looked for test facilities allowing to download the required 3rd party modules. `createNext()` in production context made my day, but showed two issues:
- `cliOutput` is not cleaned in between tests. While clearance during `stop()` would be annoying, I hope that clearance during `start()` is better.
- if `start()` fails while building, the created instance can never be stopped. This is because we don't clear `childProcess` after `build`.
## Bug
- [x] Related issues linked using `fixes #number`
- [x] 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
- [x] Make sure the linting passes by running `yarn lint`
2022-05-17 21:35:48 +02:00
|
|
|
* Order matters: for it to work, it must be registered first
|
2022-04-30 13:19:27 +02:00
|
|
|
*/
|
2022-05-23 11:07:26 +02:00
|
|
|
const skip = () => (isInMiddlewareLayer(parser) ? true : undefined)
|
2022-04-30 13:19:27 +02:00
|
|
|
|
fix(middleware): false positive dynamic code detection at build time (#36955)
## What's in there?
Partially fixes https://github.com/vercel/edge-functions/issues/82
Relates to #36715
Our webpack plugin for middleware leverages static analysis to detect Dyanamic code evaluation in user `_middleware.js` file (and depedencies). Since edge function runtime do not allow them, the build is aborted.
The use of `Function.bind` is considered invalid, while it is legit. A customer using `@aws-sdk/client-s3` reported it.
This PR fixes it.
Please note that this check is too strict: some dynamic code may be in the bundle (despite treeshaking), but may never be used (because of code branches). Since this point is under discussion, this PR adds tests covering some false positives (`@apollo/react-hook`, `qs` and `has`), but does not change the behavior (consider them as errors).
## Notes to reviewer
I looked for test facilities allowing to download the required 3rd party modules. `createNext()` in production context made my day, but showed two issues:
- `cliOutput` is not cleaned in between tests. While clearance during `stop()` would be annoying, I hope that clearance during `start()` is better.
- if `start()` fails while building, the created instance can never be stopped. This is because we don't clear `childProcess` after `build`.
## Bug
- [x] Related issues linked using `fixes #number`
- [x] 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
- [x] Make sure the linting passes by running `yarn lint`
2022-05-17 21:35:48 +02:00
|
|
|
for (const prefix of ['', 'global.']) {
|
|
|
|
hooks.expression.for(`${prefix}Function.prototype`).tap(NAME, skip)
|
|
|
|
hooks.expression.for(`${prefix}Function.bind`).tap(NAME, skip)
|
|
|
|
hooks.call.for(`${prefix}eval`).tap(NAME, handleWrapExpression)
|
|
|
|
hooks.call.for(`${prefix}Function`).tap(NAME, handleWrapExpression)
|
|
|
|
hooks.new.for(`${prefix}Function`).tap(NAME, handleWrapExpression)
|
|
|
|
hooks.expression.for(`${prefix}eval`).tap(NAME, handleExpression)
|
|
|
|
hooks.expression.for(`${prefix}Function`).tap(NAME, handleExpression)
|
2022-06-16 16:59:30 +02:00
|
|
|
hooks.call
|
|
|
|
.for(`${prefix}WebAssembly.compile`)
|
|
|
|
.tap(NAME, handleWrapWasmCompileExpression)
|
|
|
|
hooks.call
|
|
|
|
.for(`${prefix}WebAssembly.instantiate`)
|
|
|
|
.tap(NAME, handleWrapWasmInstantiateExpression)
|
fix(middleware): false positive dynamic code detection at build time (#36955)
## What's in there?
Partially fixes https://github.com/vercel/edge-functions/issues/82
Relates to #36715
Our webpack plugin for middleware leverages static analysis to detect Dyanamic code evaluation in user `_middleware.js` file (and depedencies). Since edge function runtime do not allow them, the build is aborted.
The use of `Function.bind` is considered invalid, while it is legit. A customer using `@aws-sdk/client-s3` reported it.
This PR fixes it.
Please note that this check is too strict: some dynamic code may be in the bundle (despite treeshaking), but may never be used (because of code branches). Since this point is under discussion, this PR adds tests covering some false positives (`@apollo/react-hook`, `qs` and `has`), but does not change the behavior (consider them as errors).
## Notes to reviewer
I looked for test facilities allowing to download the required 3rd party modules. `createNext()` in production context made my day, but showed two issues:
- `cliOutput` is not cleaned in between tests. While clearance during `stop()` would be annoying, I hope that clearance during `start()` is better.
- if `start()` fails while building, the created instance can never be stopped. This is because we don't clear `childProcess` after `build`.
## Bug
- [x] Related issues linked using `fixes #number`
- [x] 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
- [x] Make sure the linting passes by running `yarn lint`
2022-05-17 21:35:48 +02:00
|
|
|
}
|
feat(middleware)!: forbids middleware response body (#36835)
_Hello Next.js team! First PR here, I hope I've followed the right practices._
### What's in there?
It has been decided to only support the following uses cases in Next.js' middleware:
- rewrite the URL (`x-middleware-rewrite` response header)
- redirect to another URL (`Location` response header)
- pass on to the next piece in the request pipeline (`x-middleware-next` response header)
1. during development, a warning on console tells developers when they are returning a response (either with `Response` or `NextResponse`).
2. at build time, this warning becomes an error.
3. at run time, returning a response body will trigger a 500 HTTP error with a JSON payload containing the detailed error.
All returned/thrown errors contain a link to the documentation.
This is a breaking feature compared to the _beta_ middleware implementation, and also removes `NextResponse.json()` which makes no sense any more.
### How to try it?
- runtime behavior: `HEADLESS=true yarn jest test/integration/middleware/core`
- build behavior : `yarn jest test/integration/middleware/build-errors`
- development behavior: `HEADLESS=true yarn jest test/development/middleware-warnings`
### Notes to reviewers
The limitation happens in next's web adapter. ~The initial implementation was to check `response.body` existence, but it turns out [`Response.redirect()`](https://github.com/vercel/next.js/blob/canary/packages/next/server/web/spec-compliant/response.ts#L42-L53) may set the response body (https://github.com/vercel/next.js/pull/31886). Hence why the proposed implementation specifically looks at response headers.~
`Response.redirect()` and `NextResponse.redirect()` do not need to include the final location in their body: it is handled by next server https://github.com/vercel/next.js/blob/canary/packages/next/server/next-server.ts#L1142
Because this is a breaking change, I had to adjust several tests cases, previously returning JSON/stream/text bodies. When relevant, these middlewares are returning data using response headers.
About DevEx: relying on AST analysis to detect forbidden use cases is not as good as running the code.
Such cases are easy to detect:
```js
new Response('a text value')
new Response(JSON.stringify({ /* whatever */ })
```
But these are false-positive cases:
```js
function returnNull() { return null }
new Response(returnNull())
function doesNothing() {}
new Response(doesNothing())
```
However, I see no good reasons to let users ship middleware such as the one above, hence why the build will fail, even if _technically speaking_, they are not setting the response body.
## Feature
- [x] 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`
- [x] Integration tests added
- [x] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [x] Errors have helpful link attached, see `contributing.md`
## Documentation / Examples
- [x] Make sure the linting passes by running `yarn lint`
2022-05-20 00:02:20 +02:00
|
|
|
hooks.new.for('Response').tap(NAME, handleNewResponseExpression)
|
|
|
|
hooks.new.for('NextResponse').tap(NAME, handleNewResponseExpression)
|
2022-04-30 13:19:27 +02:00
|
|
|
hooks.callMemberChain.for('process').tap(NAME, handleCallMemberChain)
|
|
|
|
hooks.expressionMemberChain.for('process').tap(NAME, handleCallMemberChain)
|
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`)
2022-07-06 22:54:44 +02:00
|
|
|
hooks.importCall.tap(NAME, handleImport)
|
|
|
|
hooks.import.tap(NAME, handleImport)
|
2022-06-09 13:49:58 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Support static analyzing environment variables through
|
|
|
|
* destructuring `process.env` or `process["env"]`:
|
|
|
|
*
|
|
|
|
* const { MY_ENV, "MY-ENV": myEnv } = process.env
|
|
|
|
* ^^^^^^ ^^^^^^
|
|
|
|
*/
|
|
|
|
hooks.declarator.tap(NAME, (declarator) => {
|
|
|
|
if (
|
|
|
|
declarator.init?.type === 'MemberExpression' &&
|
|
|
|
isProcessEnvMemberExpression(declarator.init) &&
|
|
|
|
declarator.id?.type === 'ObjectPattern'
|
|
|
|
) {
|
|
|
|
for (const property of declarator.id.properties) {
|
|
|
|
if (property.type === 'RestElement') continue
|
|
|
|
if (
|
|
|
|
property.key.type === 'Literal' &&
|
|
|
|
typeof property.key.value === 'string'
|
|
|
|
) {
|
|
|
|
addUsedEnvVar(property.key.value)
|
|
|
|
} else if (property.key.type === 'Identifier') {
|
|
|
|
addUsedEnvVar(property.key.name)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!isInMiddlewareLayer(parser)) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
fix(edge): error handling for edge route and middleware is inconsistent (#38401)
## What’s in there?
This PR brings more consistency in how errors and warnings are reported when running code in the Edge Runtime:
- Dynamic code evaluation (`eval()`, `new Function()`, `WebAssembly.instantiate()`, `WebAssembly.compile()`…)
- Usage of Node.js global APIs (`BroadcastChannel`, `Buffer`, `TextDecoderStream`, `setImmediate()`...)
- Usage of Node.js modules (`fs`, `path`, `child_process`…)
The new error messages should mention *Edge Runtime* instead of *Middleware*, so they are valid in both cases.
It also fixes a bug where the process polyfill would issue a warning for `process.cwd` (which is `undefined` but legit). Now, one has to invoke the function `process.cwd()` to trigger the error.
It finally fixes the react-dev-overlay, where links from middleware and Edge API route files could not be opened because of the `(middleware)/` prefix in their name.
About the later, please note that we can’t easily remove the prefix or change it for Edge API routes. It comes from the Webpack layer, which is the same for both. We may consider renaming it to *edge* instead in the future.
## How to test?
These changes are almost fully covered with tests:
```bash
pnpm testheadless --testPathPattern runtime-dynamic
pnpm testheadless --testPathPattern runtime-with-node
pnpm testheadless --testPathPattern runtime-module
pnpm testheadless --testPathPattern middleware-dev-errors
```
To try them out manually, you can write a middleware and Edge route files like these:
```jsx
// middleware.js
import { NextResponse } from 'next/server'
import { basename } from 'path'
export default async function middleware() {
eval('2+2')
setImmediate(() => {})
basename()
return NextResponse.next()
}
export const config = { matcher: '/' }
```
```jsx
// pages/api/route.js
import { basename } from 'path'
export default async function handle() {
eval('2+2')
setImmediate(() => {})
basename()
return Response.json({ ok: true })
}
export const config = { runtime: 'experimental-edge' }
```
The expected behaviours are:
- [x] dev, middleware/edge route is using a node.js module: error at runtime (logs + read-dev-overlay):
```bash
error - (middleware)/pages/api/route.js (1:0) @ Object.handle [as handler]
error - The edge runtime does not support Node.js 'path' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
> 1 | import { basename } from "path";
2 | export default async function handle() {
```
- [x] build, middleware/edge route is using a node.js module: warning but succeeds
```bash
warn - Compiled with warnings
./middleware.js
A Node.js module is loaded ('path' at line 4) which is not supported in the Edge Runtime.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
./pages/api/route.js
A Node.js module is loaded ('path' at line 1) which is not supported in the Edge Runtime.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
```
- [x] production, middleware/edge route is using a node.js module: error at runtime (logs + 500 error)
```bash
Error: The edge runtime does not support Node.js 'path' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
at <unknown> (file:///Users/damien/dev/next.js/packages/next/server/web/sandbox/context.ts:149)
```
- [x] dev, middleware/edge route is using a node.js global API: error at runtime (logs + read-dev-overlay):
```bash
error - (middleware)/pages/api/route.js (4:2) @ Object.handle [as handler]
error - A Node.js API is used (setImmediate) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
2 |
3 | export default async function handle() {
> 4 | setImmediate(() => {})
| ^
```
- [x] build, middleware/edge route is using a node.js global API: warning but succeeds
```bash
warn - Compiled with warnings
./middleware.js
A Node.js API is used (setImmediate at line: 6) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
./pages/api/route.js
A Node.js API is used (setImmediate at line: 3) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
```
- [x] production, middleware/edge route is using a node.js module: error at runtime (logs + 500 error)
```bash
Error: A Node.js API is used (setImmediate) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
at <unknown> (file:///Users/damien/dev/next.js/packages/next/server/web/sandbox/context.ts:330)
```
- [x] dev, middleware/edge route is loading dynamic code: warning at runtime (logs + read-dev-overlay) and request succeeds (we allow dynamic code in dev only):
```bash
warn - (middleware)/middleware.js (7:2) @ Object.middleware [as handler]
warn - Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime
5 |
6 | export default async function middleware() {
> 7 | eval('2+2')
```
- [x] build, middleware/edge route is loading dynamic code: build fails with error:
```bash
Failed to compile.
./middleware.js
Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime
Used by default
./pages/api/route.js
Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime
Used by default
```
## Notes to reviewers
Edge-related errors are either issued from `next/server/web/sandbox/context.ts` file (runtime errors) or from `next/build/webpack/plugins/middleware-plugin.ts` (webpack compilation).
The previous implementation (I’m pleading guilty here) was way too verbose: some errors (Node.js global APIs like using `process.cwd()`) could be reported several times, and the previous mechanism to dedupe them (in middleware-plugin) wasn’t really effective.
Changes in tests are due to renaming existing tests such as `test/integration/middleware-with-node.js-apis` into `test/integration/edge-runtime-with-node.js-apis`. I extended them to cover Edge API route.
@hanneslund I’ve pushed the improvement you did in https://github.com/vercel/next.js/pull/38289/ one step further to avoid duplication.
2022-07-21 16:53:23 +02:00
|
|
|
if (!dev) {
|
|
|
|
// do not issue compilation warning on dev: invoking code will provide details
|
|
|
|
registerUnsupportedApiHooks(parser, compilation)
|
|
|
|
}
|
2022-04-30 13:19:27 +02:00
|
|
|
}
|
|
|
|
}
|
2021-11-05 21:48:43 +01:00
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
function getExtractMetadata(params: {
|
2022-08-16 11:55:37 +02:00
|
|
|
compilation: webpack.Compilation
|
|
|
|
compiler: webpack.Compiler
|
2022-04-30 13:19:27 +02:00
|
|
|
dev: boolean
|
|
|
|
metadataByEntry: Map<string, EntryMetadata>
|
|
|
|
}) {
|
|
|
|
const { dev, compilation, metadataByEntry, compiler } = params
|
|
|
|
const { webpack: wp } = compiler
|
|
|
|
return () => {
|
|
|
|
metadataByEntry.clear()
|
|
|
|
|
|
|
|
for (const [entryName, entryData] of compilation.entries) {
|
|
|
|
if (entryData.options.runtime !== EDGE_RUNTIME_WEBPACK) {
|
|
|
|
// Only process edge runtime entries
|
|
|
|
continue
|
|
|
|
}
|
2021-11-10 16:31:46 +01:00
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
const { moduleGraph } = compilation
|
2022-08-16 11:55:37 +02:00
|
|
|
const entryModules = new Set<webpack.Module>()
|
2022-04-30 13:19:27 +02:00
|
|
|
const addEntriesFromDependency = (dependency: any) => {
|
|
|
|
const module = moduleGraph.getModule(dependency)
|
|
|
|
if (module) {
|
|
|
|
entryModules.add(module)
|
2022-01-31 16:46:04 +01:00
|
|
|
}
|
2022-04-30 13:19:27 +02:00
|
|
|
}
|
2021-10-20 19:52:11 +02:00
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
entryData.dependencies.forEach(addEntriesFromDependency)
|
|
|
|
entryData.includeDependencies.forEach(addEntriesFromDependency)
|
2021-10-20 19:52:11 +02:00
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
const entryMetadata: EntryMetadata = {
|
|
|
|
env: new Set<string>(),
|
2022-07-19 19:27:15 +02:00
|
|
|
wasmBindings: new Map(),
|
|
|
|
assetBindings: new Map(),
|
2022-04-30 13:19:27 +02:00
|
|
|
}
|
2021-10-20 19:52:11 +02:00
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
for (const entryModule of entryModules) {
|
|
|
|
const buildInfo = getModuleBuildInfo(entryModule)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* When building for production checks if the module is using `eval`
|
|
|
|
* and in such case produces a compilation error. The module has to
|
|
|
|
* be in use.
|
|
|
|
*/
|
|
|
|
if (
|
|
|
|
!dev &&
|
|
|
|
buildInfo.usingIndirectEval &&
|
|
|
|
isUsingIndirectEvalAndUsedByExports({
|
|
|
|
entryModule: entryModule,
|
|
|
|
moduleGraph: moduleGraph,
|
|
|
|
runtime: wp.util.runtime.getEntryRuntime(compilation, entryName),
|
|
|
|
usingIndirectEval: buildInfo.usingIndirectEval,
|
|
|
|
wp,
|
|
|
|
})
|
|
|
|
) {
|
|
|
|
const id = entryModule.identifier()
|
|
|
|
if (/node_modules[\\/]regenerator-runtime[\\/]runtime\.js/.test(id)) {
|
|
|
|
continue
|
2022-01-31 16:46:04 +01:00
|
|
|
}
|
|
|
|
|
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`)
2022-07-06 22:54:44 +02:00
|
|
|
compilation.errors.push(
|
|
|
|
buildWebpackError({
|
fix(edge): error handling for edge route and middleware is inconsistent (#38401)
## What’s in there?
This PR brings more consistency in how errors and warnings are reported when running code in the Edge Runtime:
- Dynamic code evaluation (`eval()`, `new Function()`, `WebAssembly.instantiate()`, `WebAssembly.compile()`…)
- Usage of Node.js global APIs (`BroadcastChannel`, `Buffer`, `TextDecoderStream`, `setImmediate()`...)
- Usage of Node.js modules (`fs`, `path`, `child_process`…)
The new error messages should mention *Edge Runtime* instead of *Middleware*, so they are valid in both cases.
It also fixes a bug where the process polyfill would issue a warning for `process.cwd` (which is `undefined` but legit). Now, one has to invoke the function `process.cwd()` to trigger the error.
It finally fixes the react-dev-overlay, where links from middleware and Edge API route files could not be opened because of the `(middleware)/` prefix in their name.
About the later, please note that we can’t easily remove the prefix or change it for Edge API routes. It comes from the Webpack layer, which is the same for both. We may consider renaming it to *edge* instead in the future.
## How to test?
These changes are almost fully covered with tests:
```bash
pnpm testheadless --testPathPattern runtime-dynamic
pnpm testheadless --testPathPattern runtime-with-node
pnpm testheadless --testPathPattern runtime-module
pnpm testheadless --testPathPattern middleware-dev-errors
```
To try them out manually, you can write a middleware and Edge route files like these:
```jsx
// middleware.js
import { NextResponse } from 'next/server'
import { basename } from 'path'
export default async function middleware() {
eval('2+2')
setImmediate(() => {})
basename()
return NextResponse.next()
}
export const config = { matcher: '/' }
```
```jsx
// pages/api/route.js
import { basename } from 'path'
export default async function handle() {
eval('2+2')
setImmediate(() => {})
basename()
return Response.json({ ok: true })
}
export const config = { runtime: 'experimental-edge' }
```
The expected behaviours are:
- [x] dev, middleware/edge route is using a node.js module: error at runtime (logs + read-dev-overlay):
```bash
error - (middleware)/pages/api/route.js (1:0) @ Object.handle [as handler]
error - The edge runtime does not support Node.js 'path' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
> 1 | import { basename } from "path";
2 | export default async function handle() {
```
- [x] build, middleware/edge route is using a node.js module: warning but succeeds
```bash
warn - Compiled with warnings
./middleware.js
A Node.js module is loaded ('path' at line 4) which is not supported in the Edge Runtime.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
./pages/api/route.js
A Node.js module is loaded ('path' at line 1) which is not supported in the Edge Runtime.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
```
- [x] production, middleware/edge route is using a node.js module: error at runtime (logs + 500 error)
```bash
Error: The edge runtime does not support Node.js 'path' module.
Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime
at <unknown> (file:///Users/damien/dev/next.js/packages/next/server/web/sandbox/context.ts:149)
```
- [x] dev, middleware/edge route is using a node.js global API: error at runtime (logs + read-dev-overlay):
```bash
error - (middleware)/pages/api/route.js (4:2) @ Object.handle [as handler]
error - A Node.js API is used (setImmediate) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
2 |
3 | export default async function handle() {
> 4 | setImmediate(() => {})
| ^
```
- [x] build, middleware/edge route is using a node.js global API: warning but succeeds
```bash
warn - Compiled with warnings
./middleware.js
A Node.js API is used (setImmediate at line: 6) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
./pages/api/route.js
A Node.js API is used (setImmediate at line: 3) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
```
- [x] production, middleware/edge route is using a node.js module: error at runtime (logs + 500 error)
```bash
Error: A Node.js API is used (setImmediate) which is not supported in the Edge Runtime.
Learn more: https://nextjs.org/docs/api-reference/edge-runtime
at <unknown> (file:///Users/damien/dev/next.js/packages/next/server/web/sandbox/context.ts:330)
```
- [x] dev, middleware/edge route is loading dynamic code: warning at runtime (logs + read-dev-overlay) and request succeeds (we allow dynamic code in dev only):
```bash
warn - (middleware)/middleware.js (7:2) @ Object.middleware [as handler]
warn - Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime
5 |
6 | export default async function middleware() {
> 7 | eval('2+2')
```
- [x] build, middleware/edge route is loading dynamic code: build fails with error:
```bash
Failed to compile.
./middleware.js
Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime
Used by default
./pages/api/route.js
Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime
Used by default
```
## Notes to reviewers
Edge-related errors are either issued from `next/server/web/sandbox/context.ts` file (runtime errors) or from `next/build/webpack/plugins/middleware-plugin.ts` (webpack compilation).
The previous implementation (I’m pleading guilty here) was way too verbose: some errors (Node.js global APIs like using `process.cwd()`) could be reported several times, and the previous mechanism to dedupe them (in middleware-plugin) wasn’t really effective.
Changes in tests are due to renaming existing tests such as `test/integration/middleware-with-node.js-apis` into `test/integration/edge-runtime-with-node.js-apis`. I extended them to cover Edge API route.
@hanneslund I’ve pushed the improvement you did in https://github.com/vercel/next.js/pull/38289/ one step further to avoid duplication.
2022-07-21 16:53:23 +02:00
|
|
|
message: `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime ${
|
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`)
2022-07-06 22:54:44 +02:00
|
|
|
typeof buildInfo.usingIndirectEval !== 'boolean'
|
|
|
|
? `\nUsed by ${Array.from(buildInfo.usingIndirectEval).join(
|
|
|
|
', '
|
|
|
|
)}`
|
|
|
|
: ''
|
|
|
|
}`,
|
|
|
|
entryModule,
|
|
|
|
compilation,
|
|
|
|
})
|
2022-04-30 13:19:27 +02:00
|
|
|
)
|
|
|
|
}
|
2022-01-31 16:46:04 +01:00
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
/**
|
|
|
|
* The entry module has to be either a page or a middleware and hold
|
|
|
|
* the corresponding metadata.
|
|
|
|
*/
|
|
|
|
if (buildInfo?.nextEdgeSSR) {
|
|
|
|
entryMetadata.edgeSSR = buildInfo.nextEdgeSSR
|
|
|
|
} else if (buildInfo?.nextEdgeMiddleware) {
|
|
|
|
entryMetadata.edgeMiddleware = buildInfo.nextEdgeMiddleware
|
2022-06-13 20:17:44 +02:00
|
|
|
} else if (buildInfo?.nextEdgeApiFunction) {
|
|
|
|
entryMetadata.edgeApiFunction = buildInfo.nextEdgeApiFunction
|
2022-04-30 13:19:27 +02:00
|
|
|
}
|
2022-01-31 16:46:04 +01:00
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
/**
|
|
|
|
* If there are env vars found in the module, append them to the set
|
|
|
|
* of env vars for the entry.
|
|
|
|
*/
|
|
|
|
if (buildInfo?.nextUsedEnvVars !== undefined) {
|
|
|
|
for (const envName of buildInfo.nextUsedEnvVars) {
|
|
|
|
entryMetadata.env.add(envName)
|
|
|
|
}
|
|
|
|
}
|
2022-01-31 16:46:04 +01:00
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
/**
|
|
|
|
* If the module is a WASM module we read the binding information and
|
|
|
|
* append it to the entry wasm bindings.
|
|
|
|
*/
|
|
|
|
if (buildInfo?.nextWasmMiddlewareBinding) {
|
2022-07-19 19:27:15 +02:00
|
|
|
entryMetadata.wasmBindings.set(
|
|
|
|
buildInfo.nextWasmMiddlewareBinding.name,
|
|
|
|
buildInfo.nextWasmMiddlewareBinding.filePath
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if (buildInfo?.nextAssetMiddlewareBinding) {
|
|
|
|
entryMetadata.assetBindings.set(
|
|
|
|
buildInfo.nextAssetMiddlewareBinding.name,
|
|
|
|
buildInfo.nextAssetMiddlewareBinding.filePath
|
|
|
|
)
|
2022-04-30 13:19:27 +02:00
|
|
|
}
|
2022-01-31 16:46:04 +01:00
|
|
|
|
2022-04-30 13:19:27 +02:00
|
|
|
/**
|
|
|
|
* Append to the list of modules to process outgoingConnections from
|
|
|
|
* the module that is being processed.
|
|
|
|
*/
|
|
|
|
for (const conn of moduleGraph.getOutgoingConnections(entryModule)) {
|
|
|
|
if (conn.module) {
|
|
|
|
entryModules.add(conn.module)
|
|
|
|
}
|
2022-01-31 16:46:04 +01:00
|
|
|
}
|
2022-04-30 13:19:27 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
metadataByEntry.set(entryName, entryMetadata)
|
2022-01-31 16:46:04 +01:00
|
|
|
}
|
2022-04-30 13:19:27 +02:00
|
|
|
}
|
2021-10-20 19:52:11 +02:00
|
|
|
}
|
2021-11-05 21:48:43 +01:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
export default class MiddlewarePlugin {
|
|
|
|
dev: boolean
|
2022-04-30 13:19:27 +02:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
constructor({ dev }: { dev: boolean }) {
|
|
|
|
this.dev = dev
|
2021-11-05 21:48:43 +01:00
|
|
|
}
|
2022-04-30 13:19:27 +02:00
|
|
|
|
2022-08-16 11:55:37 +02:00
|
|
|
apply(compiler: webpack.Compiler) {
|
2022-08-15 16:29:51 +02:00
|
|
|
compiler.hooks.compilation.tap(NAME, (compilation, params) => {
|
|
|
|
const { hooks } = params.normalModuleFactory
|
|
|
|
/**
|
|
|
|
* This is the static code analysis phase.
|
|
|
|
*/
|
|
|
|
const codeAnalyzer = getCodeAnalyzer({
|
|
|
|
dev: this.dev,
|
|
|
|
compiler,
|
|
|
|
compilation,
|
2022-05-19 17:46:21 +02:00
|
|
|
})
|
2022-08-15 16:29:51 +02:00
|
|
|
hooks.parser.for('javascript/auto').tap(NAME, codeAnalyzer)
|
|
|
|
hooks.parser.for('javascript/dynamic').tap(NAME, codeAnalyzer)
|
|
|
|
hooks.parser.for('javascript/esm').tap(NAME, codeAnalyzer)
|
feat(middleware)!: forbids middleware response body (#36835)
_Hello Next.js team! First PR here, I hope I've followed the right practices._
### What's in there?
It has been decided to only support the following uses cases in Next.js' middleware:
- rewrite the URL (`x-middleware-rewrite` response header)
- redirect to another URL (`Location` response header)
- pass on to the next piece in the request pipeline (`x-middleware-next` response header)
1. during development, a warning on console tells developers when they are returning a response (either with `Response` or `NextResponse`).
2. at build time, this warning becomes an error.
3. at run time, returning a response body will trigger a 500 HTTP error with a JSON payload containing the detailed error.
All returned/thrown errors contain a link to the documentation.
This is a breaking feature compared to the _beta_ middleware implementation, and also removes `NextResponse.json()` which makes no sense any more.
### How to try it?
- runtime behavior: `HEADLESS=true yarn jest test/integration/middleware/core`
- build behavior : `yarn jest test/integration/middleware/build-errors`
- development behavior: `HEADLESS=true yarn jest test/development/middleware-warnings`
### Notes to reviewers
The limitation happens in next's web adapter. ~The initial implementation was to check `response.body` existence, but it turns out [`Response.redirect()`](https://github.com/vercel/next.js/blob/canary/packages/next/server/web/spec-compliant/response.ts#L42-L53) may set the response body (https://github.com/vercel/next.js/pull/31886). Hence why the proposed implementation specifically looks at response headers.~
`Response.redirect()` and `NextResponse.redirect()` do not need to include the final location in their body: it is handled by next server https://github.com/vercel/next.js/blob/canary/packages/next/server/next-server.ts#L1142
Because this is a breaking change, I had to adjust several tests cases, previously returning JSON/stream/text bodies. When relevant, these middlewares are returning data using response headers.
About DevEx: relying on AST analysis to detect forbidden use cases is not as good as running the code.
Such cases are easy to detect:
```js
new Response('a text value')
new Response(JSON.stringify({ /* whatever */ })
```
But these are false-positive cases:
```js
function returnNull() { return null }
new Response(returnNull())
function doesNothing() {}
new Response(doesNothing())
```
However, I see no good reasons to let users ship middleware such as the one above, hence why the build will fail, even if _technically speaking_, they are not setting the response body.
## Feature
- [x] 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`
- [x] Integration tests added
- [x] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [x] Errors have helpful link attached, see `contributing.md`
## Documentation / Examples
- [x] Make sure the linting passes by running `yarn lint`
2022-05-20 00:02:20 +02:00
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
/**
|
|
|
|
* Extract all metadata for the entry points in a Map object.
|
|
|
|
*/
|
|
|
|
const metadataByEntry = new Map<string, EntryMetadata>()
|
|
|
|
compilation.hooks.afterOptimizeModules.tap(
|
|
|
|
NAME,
|
|
|
|
getExtractMetadata({
|
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`)
2022-07-06 22:54:44 +02:00
|
|
|
compilation,
|
2022-08-15 16:29:51 +02:00
|
|
|
compiler,
|
|
|
|
dev: this.dev,
|
|
|
|
metadataByEntry,
|
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`)
2022-07-06 22:54:44 +02:00
|
|
|
})
|
2022-05-23 11:07:26 +02:00
|
|
|
)
|
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
/**
|
|
|
|
* Emit the middleware manifest.
|
|
|
|
*/
|
|
|
|
compilation.hooks.processAssets.tap(
|
|
|
|
{
|
|
|
|
name: 'NextJsMiddlewareManifest',
|
|
|
|
stage: (webpack as any).Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
|
|
|
|
},
|
|
|
|
getCreateAssets({ compilation, metadataByEntry })
|
|
|
|
)
|
|
|
|
})
|
2022-05-23 11:07:26 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-15 16:29:51 +02:00
|
|
|
export async function handleWebpackExtenalForEdgeRuntime({
|
|
|
|
request,
|
|
|
|
context,
|
|
|
|
contextInfo,
|
|
|
|
getResolve,
|
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`)
2022-07-06 22:54:44 +02:00
|
|
|
}: {
|
2022-08-15 16:29:51 +02:00
|
|
|
request: string
|
|
|
|
context: string
|
|
|
|
contextInfo: any
|
|
|
|
getResolve: () => any
|
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`)
2022-07-06 22:54:44 +02:00
|
|
|
}) {
|
2022-08-15 16:29:51 +02:00
|
|
|
if (contextInfo.issuerLayer === 'middleware' && isNodeJsModule(request)) {
|
|
|
|
// allows user to provide and use their polyfills, as we do with buffer.
|
|
|
|
try {
|
|
|
|
await getResolve()(context, request)
|
|
|
|
} catch {
|
|
|
|
return `root globalThis.__import_unsupported('${request}')`
|
|
|
|
}
|
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`)
2022-07-06 22:54:44 +02:00
|
|
|
}
|
|
|
|
}
|