diff --git a/docs/api-reference/edge-runtime.md b/docs/api-reference/edge-runtime.md index c62a84973bf4fd3..7993c3a6e80f658 100644 --- a/docs/api-reference/edge-runtime.md +++ b/docs/api-reference/edge-runtime.md @@ -136,6 +136,25 @@ The following JavaScript language features are disabled, and **will not work:** - `eval`: Evaluates JavaScript code represented as a string - `new Function(evalString)`: Creates a new function with the code provided as an argument +- `WebAssembly.compile` +- `WebAssembly.instantiate` with [a buffer parameter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate#primary_overload_%E2%80%94_taking_wasm_binary_code) + +In rare cases, your code could contain (or import) some dynamic code evaluation statements which _can not be reached at runtime_ and which can not be removed by treeshaking. +You can relax the check to allow specific files with your Middleware or Edge API Route exported configuration: + +```javascript +export const config = { + runtime: 'experimental-edge', // for Edge API Routes only + allowDynamic: [ + '/lib/utilities.js', // allows a single file + '/node_modules/function-bind/**', // use a glob to allow anything in the function-bind 3rd party module + ], +} +``` + +`allowDynamic` is a [glob](https://github.com/micromatch/micromatch#matching-features), or an array of globs, ignoring dynamic code evaluation for specific files. The globs are relative to your application root folder. + +Be warned that if these statements are executed on the Edge, _they will throw and cause a runtime error_. ## Related diff --git a/errors/edge-dynamic-code-evaluation.md b/errors/edge-dynamic-code-evaluation.md new file mode 100644 index 000000000000000..29a5e74b6329fb5 --- /dev/null +++ b/errors/edge-dynamic-code-evaluation.md @@ -0,0 +1,34 @@ +# Dynamic code evaluation is not available in Middlewares or Edge API Routes + +#### Why This Error Occurred + +`eval()`, `new Function()` or compiling WASM binaries dynamically is not allowed in Middlewares or Edge API Routes. +Specifically, the following APIs are not supported: + +- `eval()` +- `new Function()` +- `WebAssembly.compile` +- `WebAssembly.instantiate` with [a buffer parameter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiate#primary_overload_%E2%80%94_taking_wasm_binary_code) + +#### Possible Ways to Fix It + +You can bundle your WASM binaries using `import`: + +```typescript +import { NextResponse } from 'next/server' +import squareWasm from './square.wasm?module' + +export default async function middleware() { + const m = await WebAssembly.instantiate(squareWasm) + const answer = m.exports.square(9) + + const response = NextResponse.next() + response.headers.set('x-square', answer.toString()) + return response +} +``` + +In rare cases, your code could contain (or import) some dynamic code evaluation statements which _can not be reached at runtime_ and which can not be removed by treeshaking. +You can relax the check to allow specific files with your Middleware or Edge API Route exported [configuration](https://nextjs.org/docs/api-reference/edge-runtime#unsupported-apis). + +Be warned that if these statements are executed on the Edge, _they will throw and cause a runtime error_. diff --git a/errors/manifest.json b/errors/manifest.json index 42134b6ca26fcf3..41bb35c7d6004b5 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -714,6 +714,10 @@ "title": "middleware-dynamic-wasm-compilation", "path": "/errors/middleware-dynamic-wasm-compilation.md" }, + { + "title": "edge-dynamic-code-evaluation", + "path": "/errors/edge-dynamic-code-evaluation.md" + }, { "title": "node-module-in-edge-runtime", "path": "/errors/node-module-in-edge-runtime.md" diff --git a/errors/middleware-dynamic-wasm-compilation.md b/errors/middleware-dynamic-wasm-compilation.md index 7b5272a41d505bd..ffe3b85546f4600 100644 --- a/errors/middleware-dynamic-wasm-compilation.md +++ b/errors/middleware-dynamic-wasm-compilation.md @@ -19,8 +19,8 @@ import squareWasm from './square.wasm?module' export default async function middleware() { const m = await WebAssembly.instantiate(squareWasm) const answer = m.exports.square(9) - const response = NextResponse.next() + response.headers.set('x-square', answer.toString()) return response } diff --git a/packages/next/build/analysis/get-page-static-info.ts b/packages/next/build/analysis/get-page-static-info.ts index 37d95e9bdcdd2c4..8b6155a86fb54b0 100644 --- a/packages/next/build/analysis/get-page-static-info.ts +++ b/packages/next/build/analysis/get-page-static-info.ts @@ -12,9 +12,11 @@ import * as Log from '../output/log' import { SERVER_RUNTIME } from '../../lib/constants' import { ServerRuntime } from 'next/types' import { checkCustomRoutes } from '../../lib/load-custom-routes' +import { matcher } from 'next/dist/compiled/micromatch' export interface MiddlewareConfig { matchers: MiddlewareMatcher[] + allowDynamicGlobs: string[] } export interface MiddlewareMatcher { @@ -162,6 +164,7 @@ function getMiddlewareMatchers( } function getMiddlewareConfig( + pageFilePath: string, config: any, nextConfig: NextConfig ): Partial { @@ -171,6 +174,23 @@ function getMiddlewareConfig( result.matchers = getMiddlewareMatchers(config.matcher, nextConfig) } + if (config.allowDynamic) { + result.allowDynamicGlobs = Array.isArray(config.allowDynamic) + ? config.allowDynamic + : [config.allowDynamic] + for (const glob of result.allowDynamicGlobs ?? []) { + try { + matcher(glob) + } catch (err) { + throw new Error( + `${pageFilePath} exported 'config.allowDynamic' contains invalid pattern '${glob}': ${ + (err as Error).message + }` + ) + } + } + } + return result } @@ -223,7 +243,11 @@ export async function getPageStaticInfo(params: { const { isDev, pageFilePath, nextConfig, page } = params const fileContent = (await tryToReadFile(pageFilePath, !isDev)) || '' - if (/runtime|getStaticProps|getServerSideProps|matcher/.test(fileContent)) { + if ( + /runtime|getStaticProps|getServerSideProps|matcher|allowDynamic/.test( + fileContent + ) + ) { const swcAST = await parseModule(pageFilePath, fileContent) const { ssg, ssr } = checkExports(swcAST) @@ -268,7 +292,11 @@ export async function getPageStaticInfo(params: { warnAboutExperimentalEdgeApiFunctions() } - const middlewareConfig = getMiddlewareConfig(config, nextConfig) + const middlewareConfig = getMiddlewareConfig( + page ?? 'middleware/edge API route', + config, + nextConfig + ) return { ssr, diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index f58536b2eb7f4c6..aeb86af4f849e84 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -48,6 +48,7 @@ import { serverComponentRegex } from './webpack/loaders/utils' import { ServerRuntime } from '../types' import { normalizeAppPath } from '../shared/lib/router/utils/app-paths' import { encodeMatchers } from './webpack/loaders/next-middleware-loader' +import { EdgeFunctionLoaderOptions } from './webpack/loaders/next-edge-function-loader' type ObjectValue = T extends { [key: string]: infer V } ? V : never @@ -163,6 +164,7 @@ interface CreateEntrypointsParams { } export function getEdgeServerEntry(opts: { + rootDir: string absolutePagePath: string buildId: string bundlePath: string @@ -179,6 +181,7 @@ export function getEdgeServerEntry(opts: { const loaderParams: MiddlewareLoaderOptions = { absolutePagePath: opts.absolutePagePath, page: opts.page, + rootDir: opts.rootDir, matchers: opts.middleware?.matchers ? encodeMatchers(opts.middleware.matchers) : '', @@ -188,9 +191,10 @@ export function getEdgeServerEntry(opts: { } if (opts.page.startsWith('/api/') || opts.page === '/api') { - const loaderParams: MiddlewareLoaderOptions = { + const loaderParams: EdgeFunctionLoaderOptions = { absolutePagePath: opts.absolutePagePath, page: opts.page, + rootDir: opts.rootDir, } return `next-edge-function-loader?${stringify(loaderParams)}!` @@ -487,6 +491,7 @@ export async function createEntrypoints(params: CreateEntrypointsParams) { edgeServer[serverBundlePath] = getEdgeServerEntry({ ...params, + rootDir, absolutePagePath: mappings[page], bundlePath: clientBundlePath, isDev: false, diff --git a/packages/next/build/webpack/loaders/get-module-build-info.ts b/packages/next/build/webpack/loaders/get-module-build-info.ts index fa43fa1701ee8ab..848399cd32a07f0 100644 --- a/packages/next/build/webpack/loaders/get-module-build-info.ts +++ b/packages/next/build/webpack/loaders/get-module-build-info.ts @@ -16,6 +16,7 @@ export function getModuleBuildInfo(webpackModule: webpack.Module) { usingIndirectEval?: boolean | Set route?: RouteMeta importLocByPath?: Map + rootDir?: string } } diff --git a/packages/next/build/webpack/loaders/next-edge-function-loader.ts b/packages/next/build/webpack/loaders/next-edge-function-loader.ts index fb382d50ac03b0c..ba56808f47c4a48 100644 --- a/packages/next/build/webpack/loaders/next-edge-function-loader.ts +++ b/packages/next/build/webpack/loaders/next-edge-function-loader.ts @@ -4,16 +4,18 @@ import { stringifyRequest } from '../stringify-request' export type EdgeFunctionLoaderOptions = { absolutePagePath: string page: string + rootDir: string } export default function middlewareLoader(this: any) { - const { absolutePagePath, page }: EdgeFunctionLoaderOptions = + const { absolutePagePath, page, rootDir }: EdgeFunctionLoaderOptions = this.getOptions() const stringifiedPagePath = stringifyRequest(this, absolutePagePath) const buildInfo = getModuleBuildInfo(this._module) buildInfo.nextEdgeApiFunction = { page: page || '/', } + buildInfo.rootDir = rootDir return ` import { adapter, enhanceGlobals } from 'next/dist/server/web/adapter' diff --git a/packages/next/build/webpack/loaders/next-middleware-loader.ts b/packages/next/build/webpack/loaders/next-middleware-loader.ts index f125a5780a183e0..a7f5e40420d2e50 100644 --- a/packages/next/build/webpack/loaders/next-middleware-loader.ts +++ b/packages/next/build/webpack/loaders/next-middleware-loader.ts @@ -6,6 +6,7 @@ import { MIDDLEWARE_LOCATION_REGEXP } from '../../../lib/constants' export type MiddlewareLoaderOptions = { absolutePagePath: string page: string + rootDir: string matchers?: string } @@ -25,6 +26,7 @@ export default function middlewareLoader(this: any) { const { absolutePagePath, page, + rootDir, matchers: encodedMatchers, }: MiddlewareLoaderOptions = this.getOptions() const matchers = encodedMatchers ? decodeMatchers(encodedMatchers) : undefined @@ -35,6 +37,7 @@ export default function middlewareLoader(this: any) { page: page.replace(new RegExp(`/${MIDDLEWARE_LOCATION_REGEXP}$`), '') || '/', } + buildInfo.rootDir = rootDir return ` import { adapter, blockUnallowedResponse, enhanceGlobals } from 'next/dist/server/web/adapter' diff --git a/packages/next/build/webpack/loaders/utils.ts b/packages/next/build/webpack/loaders/utils.ts index f5274be13148ae4..6537f2943a9da20 100644 --- a/packages/next/build/webpack/loaders/utils.ts +++ b/packages/next/build/webpack/loaders/utils.ts @@ -1,3 +1,5 @@ +import { getPageStaticInfo } from '../../analysis/get-page-static-info' + export const defaultJsFileExtensions = ['js', 'mjs', 'jsx', 'ts', 'tsx'] const imageExtensions = ['jpg', 'jpeg', 'png', 'webp', 'avif'] const nextClientComponents = [ @@ -47,3 +49,17 @@ export const clientComponentRegex = new RegExp( export const serverComponentRegex = new RegExp( `\\.server(\\.(${defaultJsFileExtensions.join('|')}))?$` ) + +export async function loadEdgeFunctionConfigFromFile( + absolutePagePath: string, + resolve: (context: string, request: string) => Promise +) { + const pageFilePath = await resolve('/', absolutePagePath) + return ( + await getPageStaticInfo({ + nextConfig: {}, + pageFilePath, + isDev: false, + }) + ).middleware +} diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index b2b8ca87af78cf5..cd841609ffb2a05 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -8,6 +8,7 @@ import { getNamedMiddlewareRegex } from '../../../shared/lib/router/utils/route- import { getModuleBuildInfo } from '../loaders/get-module-build-info' import { getSortedRoutes } from '../../../shared/lib/router/utils' import { webpack, sources } from 'next/dist/compiled/webpack/webpack' +import { isMatch } from 'next/dist/compiled/micromatch' import { EDGE_RUNTIME_WEBPACK, EDGE_UNSUPPORTED_NODE_APIS, @@ -19,6 +20,12 @@ import { FLIGHT_SERVER_CSS_MANIFEST, SUBRESOURCE_INTEGRITY_MANIFEST, } from '../../../shared/lib/constants' +import { + getPageStaticInfo, + MiddlewareConfig, +} from '../../analysis/get-page-static-info' +import { Telemetry } from '../../../telemetry/storage' +import { traceGlobals } from '../../../trace/shared' export interface EdgeFunctionDefinition { env: string[] @@ -54,18 +61,18 @@ const NAME = 'MiddlewarePlugin' * simply truthy it will return true. */ function isUsingIndirectEvalAndUsedByExports(args: { - entryModule: webpack.Module + module: webpack.Module moduleGraph: webpack.ModuleGraph runtime: any usingIndirectEval: true | Set wp: typeof webpack }): boolean { - const { moduleGraph, runtime, entryModule, usingIndirectEval, wp } = args + const { moduleGraph, runtime, module, usingIndirectEval, wp } = args if (typeof usingIndirectEval === 'boolean') { return usingIndirectEval } - const exportsInfo = moduleGraph.getExportsInfo(entryModule) + const exportsInfo = moduleGraph.getExportsInfo(module) for (const exportName of usingIndirectEval) { if (exportsInfo.getUsed(exportName, runtime) !== wp.UsageState.Unused) { return true @@ -242,6 +249,15 @@ function isNodeJsModule(moduleName: string) { return require('module').builtinModules.includes(moduleName) } +function isDynamicCodeEvaluationAllowed( + fileName: string, + edgeFunctionConfig?: Partial, + rootDir?: string +) { + const name = fileName.replace(rootDir ?? '', '') + return isMatch(name, edgeFunctionConfig?.allowDynamicGlobs ?? []) +} + function buildUnsupportedApiError({ apiName, loc, @@ -364,18 +380,16 @@ function getCodeAnalyzer(params: { 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) - } + 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 @@ -391,18 +405,16 @@ function getCodeAnalyzer(params: { 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) - } + 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() } @@ -421,18 +433,16 @@ function getCodeAnalyzer(params: { return } - if (dev) { - const { ConstDependency } = wp.dependencies - const dep1 = new ConstDependency( - '__next_webassembly_instantiate__(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) - } + const { ConstDependency } = wp.dependencies + const dep1 = new ConstDependency( + '__next_webassembly_instantiate__(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) } /** @@ -586,6 +596,35 @@ Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`, } } +async function findEntryEdgeFunctionConfig( + entryDependency: any, + resolver: webpack.Resolver +) { + if (entryDependency?.request?.startsWith('next-')) { + const absolutePagePath = + new URL(entryDependency.request, 'http://example.org').searchParams.get( + 'absolutePagePath' + ) ?? '' + const pageFilePath = await new Promise((resolve) => + resolver.resolve({}, '/', absolutePagePath, {}, (err, path) => + resolve(err || path) + ) + ) + if (typeof pageFilePath === 'string') { + return { + file: pageFilePath, + config: ( + await getPageStaticInfo({ + nextConfig: {}, + pageFilePath, + isDev: false, + }) + ).middleware, + } + } + } +} + function getExtractMetadata(params: { compilation: webpack.Compilation compiler: webpack.Compiler @@ -594,26 +633,36 @@ function getExtractMetadata(params: { }) { const { dev, compilation, metadataByEntry, compiler } = params const { webpack: wp } = compiler - return () => { + return async () => { metadataByEntry.clear() + const resolver = compilation.resolverFactory.get('normal') + const telemetry: Telemetry = traceGlobals.get('telemetry') - for (const [entryName, entryData] of compilation.entries) { - if (entryData.options.runtime !== EDGE_RUNTIME_WEBPACK) { + for (const [entryName, entry] of compilation.entries) { + if (entry.options.runtime !== EDGE_RUNTIME_WEBPACK) { // Only process edge runtime entries continue } + const entryDependency = entry.dependencies?.[0] + const edgeFunctionConfig = await findEntryEdgeFunctionConfig( + entryDependency, + resolver + ) + const { rootDir } = getModuleBuildInfo( + compilation.moduleGraph.getResolvedModule(entryDependency) + ) const { moduleGraph } = compilation - const entryModules = new Set() + const modules = new Set() const addEntriesFromDependency = (dependency: any) => { const module = moduleGraph.getModule(dependency) if (module) { - entryModules.add(module) + modules.add(module as webpack.NormalModule) } } - entryData.dependencies.forEach(addEntriesFromDependency) - entryData.includeDependencies.forEach(addEntriesFromDependency) + entry.dependencies.forEach(addEntriesFromDependency) + entry.includeDependencies.forEach(addEntriesFromDependency) const entryMetadata: EntryMetadata = { env: new Set(), @@ -621,8 +670,8 @@ function getExtractMetadata(params: { assetBindings: new Map(), } - for (const entryModule of entryModules) { - const buildInfo = getModuleBuildInfo(entryModule) + for (const module of modules) { + const buildInfo = getModuleBuildInfo(module) /** * When building for production checks if the module is using `eval` @@ -633,31 +682,52 @@ function getExtractMetadata(params: { !dev && buildInfo.usingIndirectEval && isUsingIndirectEvalAndUsedByExports({ - entryModule: entryModule, - moduleGraph: moduleGraph, + module, + moduleGraph, runtime: wp.util.runtime.getEntryRuntime(compilation, entryName), usingIndirectEval: buildInfo.usingIndirectEval, wp, }) ) { - const id = entryModule.identifier() + const id = module.identifier() if (/node_modules[\\/]regenerator-runtime[\\/]runtime\.js/.test(id)) { continue } - compilation.errors.push( - buildWebpackError({ - message: `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime ${ - typeof buildInfo.usingIndirectEval !== 'boolean' - ? `\nUsed by ${Array.from(buildInfo.usingIndirectEval).join( - ', ' - )}` - : '' - }`, - entryModule, - compilation, + if (edgeFunctionConfig?.config?.allowDynamicGlobs) { + telemetry.record({ + eventName: 'NEXT_EDGE_ALLOW_DYNAMIC_USED', + payload: { + ...edgeFunctionConfig, + file: edgeFunctionConfig.file.replace(rootDir ?? '', ''), + fileWithDynamicCode: module.userRequest.replace( + rootDir ?? '', + '' + ), + }, }) - ) + } + if ( + !isDynamicCodeEvaluationAllowed( + module.userRequest, + edgeFunctionConfig?.config, + rootDir + ) + ) { + compilation.errors.push( + buildWebpackError({ + message: `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime ${ + typeof buildInfo.usingIndirectEval !== 'boolean' + ? `\nUsed by ${Array.from(buildInfo.usingIndirectEval).join( + ', ' + )}` + : '' + }\nLearn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`, + entryModule: module, + compilation, + }) + ) + } } /** @@ -704,9 +774,9 @@ function getExtractMetadata(params: { * Append to the list of modules to process outgoingConnections from * the module that is being processed. */ - for (const conn of moduleGraph.getOutgoingConnections(entryModule)) { + for (const conn of moduleGraph.getOutgoingConnections(module)) { if (conn.module) { - entryModules.add(conn.module) + modules.add(conn.module as webpack.NormalModule) } } } @@ -715,7 +785,6 @@ function getExtractMetadata(params: { } } } - export default class MiddlewarePlugin { private readonly dev: boolean private readonly sriEnabled: boolean @@ -744,7 +813,7 @@ export default class MiddlewarePlugin { * Extract all metadata for the entry points in a Map object. */ const metadataByEntry = new Map() - compilation.hooks.afterOptimizeModules.tap( + compilation.hooks.finishModules.tapPromise( NAME, getExtractMetadata({ compilation, diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 0130ed5a93ffab0..c0b2ea637e37a4e 100644 --- a/packages/next/server/dev/hot-reloader.ts +++ b/packages/next/server/dev/hot-reloader.ts @@ -640,6 +640,7 @@ export default class HotReloader { name: bundlePath, value: getEdgeServerEntry({ absolutePagePath: entryData.absolutePagePath, + rootDir: this.dir, buildId: this.buildId, bundlePath, config: this.config, diff --git a/packages/next/server/web/sandbox/context.ts b/packages/next/server/web/sandbox/context.ts index 65b076daa3ae35d..d905ddc7625fcfa 100644 --- a/packages/next/server/web/sandbox/context.ts +++ b/packages/next/server/web/sandbox/context.ts @@ -148,7 +148,8 @@ async function createModuleContext(options: ModuleContextOptions) { if (!warnedEvals.has(key)) { const warning = getServerError( new Error( - `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime +Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation` ), COMPILER_NAMES.edgeServer ) @@ -166,7 +167,7 @@ async function createModuleContext(options: ModuleContextOptions) { if (!warnedWasmCodegens.has(key)) { const warning = getServerError( new Error(`Dynamic WASM code generation (e. g. 'WebAssembly.compile') not allowed in Edge Runtime. -Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation`), +Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`), COMPILER_NAMES.edgeServer ) warning.name = 'DynamicWasmCodeGenerationWarning' @@ -193,7 +194,7 @@ Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation if (instantiatedFromBuffer && !warnedWasmCodegens.has(key)) { const warning = getServerError( new Error(`Dynamic WASM code generation ('WebAssembly.instantiate' with a buffer parameter) not allowed in Edge Runtime. -Learn More: https://nextjs.org/docs/messages/middleware-dynamic-wasm-compilation`), +Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`), COMPILER_NAMES.edgeServer ) warning.name = 'DynamicWasmCodeGenerationWarning' diff --git a/test/integration/edge-runtime-configurable-guards/lib/index.js b/test/integration/edge-runtime-configurable-guards/lib/index.js new file mode 100644 index 000000000000000..8fa47bde2f62fd8 --- /dev/null +++ b/test/integration/edge-runtime-configurable-guards/lib/index.js @@ -0,0 +1 @@ +// populated by tests diff --git a/test/integration/edge-runtime-configurable-guards/middleware.js b/test/integration/edge-runtime-configurable-guards/middleware.js new file mode 100644 index 000000000000000..361c04d84d89f85 --- /dev/null +++ b/test/integration/edge-runtime-configurable-guards/middleware.js @@ -0,0 +1,10 @@ +import { NextResponse } from 'next/server' + +// populated with tests +export default () => { + return NextResponse.next() +} + +export const config = { + matcher: '/', +} diff --git a/test/integration/edge-runtime-configurable-guards/pages/api/route.js b/test/integration/edge-runtime-configurable-guards/pages/api/route.js new file mode 100644 index 000000000000000..9d808b1a2bb6800 --- /dev/null +++ b/test/integration/edge-runtime-configurable-guards/pages/api/route.js @@ -0,0 +1,8 @@ +// populated by tests +export default () => { + return Response.json({ ok: true }) +} + +export const config = { + runtime: 'experimental-edge', +} diff --git a/test/integration/edge-runtime-configurable-guards/pages/index.js b/test/integration/edge-runtime-configurable-guards/pages/index.js new file mode 100644 index 000000000000000..c5cc676685b679a --- /dev/null +++ b/test/integration/edge-runtime-configurable-guards/pages/index.js @@ -0,0 +1,3 @@ +export default function Page() { + return
ok
+} diff --git a/test/integration/edge-runtime-configurable-guards/test/index.test.js b/test/integration/edge-runtime-configurable-guards/test/index.test.js new file mode 100644 index 000000000000000..7d72ba70111a058 --- /dev/null +++ b/test/integration/edge-runtime-configurable-guards/test/index.test.js @@ -0,0 +1,390 @@ +/* eslint-env jest */ + +import { join } from 'path' +import { + fetchViaHTTP, + File, + findPort, + killApp, + launchApp, + nextBuild, + nextStart, + waitFor, +} from 'next-test-utils' +import { remove } from 'fs-extra' + +jest.setTimeout(1000 * 60 * 2) + +const context = { + appDir: join(__dirname, '../'), + logs: { output: '', stdout: '', stderr: '' }, + api: new File(join(__dirname, '../pages/api/route.js')), + middleware: new File(join(__dirname, '../middleware.js')), + lib: new File(join(__dirname, '../lib/index.js')), +} +const appOption = { + env: { __NEXT_TEST_WITH_DEVTOOL: 1 }, + onStdout(msg) { + context.logs.output += msg + context.logs.stdout += msg + }, + onStderr(msg) { + context.logs.output += msg + context.logs.stderr += msg + }, +} +const routeUrl = '/api/route' +const middlewareUrl = '/' +const TELEMETRY_EVENT_NAME = 'NEXT_EDGE_ALLOW_DYNAMIC_USED' + +describe('Edge runtime configurable guards', () => { + beforeEach(async () => { + await remove(join(__dirname, '../.next')) + context.appPort = await findPort() + context.logs = { output: '', stdout: '', stderr: '' } + }) + + afterEach(() => { + if (context.app) { + killApp(context.app) + } + context.api.restore() + context.middleware.restore() + context.lib.restore() + }) + + describe('Multiple functions with different configurations', () => { + beforeEach(() => { + context.middleware.write(` + import { NextResponse } from 'next/server' + + export default () => { + eval('100') + return NextResponse.next() + } + export const config = { + allowDynamic: '/middleware.js' + } + `) + context.api.write(` + export default async function handler(request) { + eval('100') + return Response.json({ result: true }) + } + export const config = { + runtime: 'experimental-edge', + allowDynamic: '/lib/**' + } + `) + }) + + it('warns in dev for allowed code', async () => { + context.app = await launchApp(context.appDir, context.appPort, appOption) + const res = await fetchViaHTTP(context.appPort, middlewareUrl) + await waitFor(500) + expect(res.status).toBe(200) + expect(context.logs.output).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + + it('warns in dev for unallowed code', async () => { + context.app = await launchApp(context.appDir, context.appPort, appOption) + const res = await fetchViaHTTP(context.appPort, routeUrl) + await waitFor(500) + expect(res.status).toBe(200) + expect(context.logs.output).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + + it('fails to build because of unallowed code', async () => { + const output = await nextBuild(context.appDir, undefined, { + stdout: true, + stderr: true, + env: { NEXT_TELEMETRY_DEBUG: 1 }, + }) + expect(output.stderr).toContain(`Build failed`) + expect(output.stderr).toContain(`./pages/api/route.js`) + expect(output.stderr).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime` + ) + expect(output.stderr).toContain(`Used by default`) + expect(output.stderr).toContain(TELEMETRY_EVENT_NAME) + }) + }) + + describe.each([ + { + title: 'Edge API', + url: routeUrl, + init() { + context.api.write(` + export default async function handler(request) { + eval('100') + return Response.json({ result: true }) + } + export const config = { + runtime: 'experimental-edge', + allowDynamic: '**' + } + `) + }, + }, + { + title: 'Middleware', + url: middlewareUrl, + init() { + context.middleware.write(` + import { NextResponse } from 'next/server' + + export default () => { + eval('100') + return NextResponse.next() + } + export const config = { + allowDynamic: '**' + } + `) + }, + }, + { + title: 'Edge API using lib', + url: routeUrl, + init() { + context.api.write(` + import { hasDynamic } from '../../lib' + export default async function handler(request) { + await hasDynamic() + return Response.json({ result: true }) + } + export const config = { + runtime: 'experimental-edge', + allowDynamic: '/lib/**' + } + `) + context.lib.write(` + export async function hasDynamic() { + eval('100') + } + `) + }, + }, + { + title: 'Middleware using lib', + url: middlewareUrl, + init() { + context.middleware.write(` + import { NextResponse } from 'next/server' + import { hasDynamic } from './lib' + + // populated with tests + export default async function () { + await hasDynamic() + return NextResponse.next() + } + export const config = { + allowDynamic: '/lib/**' + } + `) + context.lib.write(` + export async function hasDynamic() { + eval('100') + } + `) + }, + }, + ])('$title with allowed, used dynamic code', ({ init, url }) => { + beforeEach(() => init()) + + it('still warns in dev at runtime', async () => { + context.app = await launchApp(context.appDir, context.appPort, appOption) + const res = await fetchViaHTTP(context.appPort, url) + await waitFor(500) + expect(res.status).toBe(200) + expect(context.logs.output).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + }) + + describe.each([ + { + title: 'Edge API', + url: routeUrl, + init() { + context.api.write(` + export default async function handler(request) { + if ((() => false)()) { + eval('100') + } + return Response.json({ result: true }) + } + export const config = { + runtime: 'experimental-edge', + allowDynamic: '**' + } + `) + }, + }, + { + title: 'Middleware', + url: middlewareUrl, + init() { + context.middleware.write(` + import { NextResponse } from 'next/server' + // populated with tests + export default () => { + if ((() => false)()) { + eval('100') + } + return NextResponse.next() + } + export const config = { + allowDynamic: '**' + } + `) + }, + }, + { + title: 'Edge API using lib', + url: routeUrl, + init() { + context.api.write(` + import { hasUnusedDynamic } from '../../lib' + export default async function handler(request) { + await hasUnusedDynamic() + return Response.json({ result: true }) + } + export const config = { + runtime: 'experimental-edge', + allowDynamic: '/lib/**' + } + `) + context.lib.write(` + export async function hasUnusedDynamic() { + if ((() => false)()) { + eval('100') + } + } + `) + }, + }, + { + title: 'Middleware using lib', + url: middlewareUrl, + init() { + context.middleware.write(` + import { NextResponse } from 'next/server' + import { hasUnusedDynamic } from './lib' + // populated with tests + export default async function () { + await hasUnusedDynamic() + return NextResponse.next() + } + export const config = { + allowDynamic: '/lib/**' + } + `) + context.lib.write(` + export async function hasUnusedDynamic() { + if ((() => false)()) { + eval('100') + } + } + `) + }, + }, + ])('$title with allowed, unused dynamic code', ({ init, url }) => { + beforeEach(() => init()) + + it('build and does not warn at runtime', async () => { + const output = await nextBuild(context.appDir, undefined, { + stdout: true, + stderr: true, + env: { NEXT_TELEMETRY_DEBUG: 1 }, + }) + expect(output.stderr).not.toContain(`Build failed`) + expect(output.stderr).toContain(TELEMETRY_EVENT_NAME) + context.app = await nextStart(context.appDir, context.appPort, appOption) + const res = await fetchViaHTTP(context.appPort, url) + expect(res.status).toBe(200) + expect(context.logs.output).not.toContain(`warn`) + expect(context.logs.output).not.toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + }) + + describe.each([ + { + title: 'Edge API using lib', + url: routeUrl, + init() { + context.api.write(` + import { hasDynamic } from '../../lib' + export default async function handler(request) { + await hasDynamic() + return Response.json({ result: true }) + } + export const config = { + runtime: 'experimental-edge', + allowDynamic: '/pages/**' + } + `) + context.lib.write(` + export async function hasDynamic() { + eval('100') + } + `) + }, + }, + { + title: 'Middleware using lib', + url: middlewareUrl, + init() { + context.middleware.write(` + import { NextResponse } from 'next/server' + import { hasDynamic } from './lib' + export default async function () { + await hasDynamic() + return NextResponse.next() + } + export const config = { + allowDynamic: '/pages/**' + } + `) + context.lib.write(` + export async function hasDynamic() { + eval('100') + } + `) + }, + }, + ])('$title with unallowed, used dynamic code', ({ init, url }) => { + beforeEach(() => init()) + + it('warns in dev at runtime', async () => { + context.app = await launchApp(context.appDir, context.appPort, appOption) + const res = await fetchViaHTTP(context.appPort, url) + await waitFor(500) + expect(res.status).toBe(200) + expect(context.logs.output).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + + it('fails to build because of dynamic code evaluation', async () => { + const output = await nextBuild(context.appDir, undefined, { + stdout: true, + stderr: true, + env: { NEXT_TELEMETRY_DEBUG: 1 }, + }) + expect(output.stderr).toContain(`Build failed`) + expect(output.stderr).toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function', 'WebAssembly.compile') not allowed in Edge Runtime` + ) + expect(output.stderr).toContain(TELEMETRY_EVENT_NAME) + }) + }) +}) diff --git a/test/production/edge-config-validations/index.test.ts b/test/production/edge-config-validations/index.test.ts new file mode 100644 index 000000000000000..ef0df8f55d00fcc --- /dev/null +++ b/test/production/edge-config-validations/index.test.ts @@ -0,0 +1,35 @@ +import { createNext } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' + +describe('Edge config validations', () => { + let next: NextInstance + + afterAll(() => next.destroy()) + + it('fails to build when allowDynamic is not a string', async () => { + next = await createNext({ + skipStart: true, + files: { + 'pages/index.js': ` + export default function Page() { + return

hello world

+ } + `, + 'middleware.js': ` + import { NextResponse } from 'next/server' + export default async function middleware(request) { + return NextResponse.next() + } + + eval('toto') + + export const config = { allowDynamic: true } + `, + }, + }) + await expect(next.start()).rejects.toThrow('next build failed') + expect(next.cliOutput).toMatch( + `/middleware exported 'config.allowDynamic' contains invalid pattern 'true': Expected pattern to be a non-empty string` + ) + }) +}) diff --git a/test/readme.md b/test/readme.md index 574c5fda5124563..b9c28639e55f513 100644 --- a/test/readme.md +++ b/test/readme.md @@ -2,7 +2,7 @@ ## Getting Started -You can set-up a new test using `yarn new-test` which will start from a template related to the test type. +You can set-up a new test using `pnpnm new-test` which will start from a template related to the test type. ## Test Types in Next.js