From 76f5e8bbfece0d4d4a4a5277fe02556e2e9f92c0 Mon Sep 17 00:00:00 2001 From: feugy Date: Tue, 16 Aug 2022 11:50:34 +0200 Subject: [PATCH 01/10] feat: introduces allowDynamic configuration key for code running on the edge --- .../build/analysis/get-page-static-info.ts | 20 +- packages/next/build/entries.ts | 7 +- .../webpack/loaders/get-module-build-info.ts | 3 + .../loaders/next-edge-function-loader.ts | 11 +- .../webpack/loaders/next-middleware-loader.ts | 10 +- packages/next/build/webpack/loaders/utils.ts | 16 + .../webpack/plugins/middleware-plugin.ts | 46 ++- packages/next/server/dev/hot-reloader.ts | 1 + .../lib/index.js | 1 + .../middleware.js | 10 + .../pages/api/route.js | 8 + .../pages/index.js | 3 + .../test/index.test.js | 302 ++++++++++++++++++ .../edge-config-validations/index.test.ts | 35 ++ test/readme.md | 2 +- 15 files changed, 463 insertions(+), 12 deletions(-) create mode 100644 test/integration/edge-runtime-configurable-guards/lib/index.js create mode 100644 test/integration/edge-runtime-configurable-guards/middleware.js create mode 100644 test/integration/edge-runtime-configurable-guards/pages/api/route.js create mode 100644 test/integration/edge-runtime-configurable-guards/pages/index.js create mode 100644 test/integration/edge-runtime-configurable-guards/test/index.test.js create mode 100644 test/production/edge-config-validations/index.test.ts diff --git a/packages/next/build/analysis/get-page-static-info.ts b/packages/next/build/analysis/get-page-static-info.ts index 37d95e9bdcdd..cb7c718c95c4 100644 --- a/packages/next/build/analysis/get-page-static-info.ts +++ b/packages/next/build/analysis/get-page-static-info.ts @@ -15,6 +15,7 @@ import { checkCustomRoutes } from '../../lib/load-custom-routes' export interface MiddlewareConfig { matchers: MiddlewareMatcher[] + allowDynamicGlobs: string[] } export interface MiddlewareMatcher { @@ -171,6 +172,19 @@ 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 ?? []) { + if (typeof glob !== 'string') { + throw new Error( + `A middleware/edge exported 'config.allowDynamic' must be a string or an array of strings` + ) + } + } + } + return result } @@ -223,7 +237,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) diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index f58536b2eb7f..aeb86af4f849 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 fa43fa1701ee..53ccc85c7b34 100644 --- a/packages/next/build/webpack/loaders/get-module-build-info.ts +++ b/packages/next/build/webpack/loaders/get-module-build-info.ts @@ -1,5 +1,6 @@ import type { MiddlewareMatcher } from '../../analysis/get-page-static-info' import { webpack } from 'next/dist/compiled/webpack/webpack' +import type { MiddlewareConfig } from '../../analysis/get-page-static-info' /** * A getter for module build info that casts to the type it should have. @@ -16,6 +17,8 @@ export function getModuleBuildInfo(webpackModule: webpack.Module) { usingIndirectEval?: boolean | Set route?: RouteMeta importLocByPath?: Map + edgeFunctionConfig?: Partial + 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 fb382d50ac03..1e64f95edfd8 100644 --- a/packages/next/build/webpack/loaders/next-edge-function-loader.ts +++ b/packages/next/build/webpack/loaders/next-edge-function-loader.ts @@ -1,19 +1,26 @@ import { getModuleBuildInfo } from './get-module-build-info' import { stringifyRequest } from '../stringify-request' +import { loadEdgeFunctionConfigFromFile } from './utils' export type EdgeFunctionLoaderOptions = { absolutePagePath: string page: string + rootDir: string } -export default function middlewareLoader(this: any) { - const { absolutePagePath, page }: EdgeFunctionLoaderOptions = +export default async function middlewareLoader(this: any) { + const { absolutePagePath, page, rootDir }: EdgeFunctionLoaderOptions = this.getOptions() const stringifiedPagePath = stringifyRequest(this, absolutePagePath) const buildInfo = getModuleBuildInfo(this._module) buildInfo.nextEdgeApiFunction = { page: page || '/', } + buildInfo.edgeFunctionConfig = await loadEdgeFunctionConfigFromFile( + absolutePagePath, + this.getResolve() + ) + 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 f125a5780a18..8f1c41e2c7a0 100644 --- a/packages/next/build/webpack/loaders/next-middleware-loader.ts +++ b/packages/next/build/webpack/loaders/next-middleware-loader.ts @@ -2,10 +2,12 @@ import type { MiddlewareMatcher } from '../../analysis/get-page-static-info' import { getModuleBuildInfo } from './get-module-build-info' import { stringifyRequest } from '../stringify-request' import { MIDDLEWARE_LOCATION_REGEXP } from '../../../lib/constants' +import { loadEdgeFunctionConfigFromFile } from './utils' export type MiddlewareLoaderOptions = { absolutePagePath: string page: string + rootDir: string matchers?: string } @@ -21,10 +23,11 @@ export function decodeMatchers(encodedMatchers: string) { ) as MiddlewareMatcher[] } -export default function middlewareLoader(this: any) { +export default async function middlewareLoader(this: any) { const { absolutePagePath, page, + rootDir, matchers: encodedMatchers, }: MiddlewareLoaderOptions = this.getOptions() const matchers = encodedMatchers ? decodeMatchers(encodedMatchers) : undefined @@ -35,6 +38,11 @@ export default function middlewareLoader(this: any) { page: page.replace(new RegExp(`/${MIDDLEWARE_LOCATION_REGEXP}$`), '') || '/', } + buildInfo.edgeFunctionConfig = await loadEdgeFunctionConfigFromFile( + absolutePagePath, + this.getResolve() + ) + 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 f5274be13148..6537f2943a9d 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 b2b8ca87af78..46b24005b7c2 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,7 @@ import { FLIGHT_SERVER_CSS_MANIFEST, SUBRESOURCE_INTEGRITY_MANIFEST, } from '../../../shared/lib/constants' +import type { MiddlewareConfig } from '../../analysis/get-page-static-info' export interface EdgeFunctionDefinition { env: string[] @@ -242,6 +244,33 @@ function isNodeJsModule(moduleName: string) { return require('module').builtinModules.includes(moduleName) } +function getEdgeEntryBuildInfo(module: webpack.Module) { + let currentModule: webpack.Module | null = module + while (currentModule) { + const buildInfo = getModuleBuildInfo(currentModule) + if (buildInfo.edgeFunctionConfig) { + return buildInfo + } + currentModule = currentModule.issuer + } +} + +function isDynamicCodeEvaluationAllowed( + fileName: string, + buildInfo?: { + edgeFunctionConfig?: Partial + rootDir?: string + } +) { + const name = fileName.replace(buildInfo?.rootDir ?? '', '') + for (const glob of buildInfo?.edgeFunctionConfig?.allowDynamicGlobs ?? []) { + if (isMatch(name, glob)) { + return true + } + } + return false +} + function buildUnsupportedApiError({ apiName, loc, @@ -325,6 +354,13 @@ function getCodeAnalyzer(params: { } = params const { hooks } = parser + function allowDynamicCodeEvaluation() { + return isDynamicCodeEvaluationAllowed( + parser.state.module.resource, + getEdgeEntryBuildInfo(parser.state.current) + ) + } + /** * 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 @@ -332,7 +368,7 @@ function getCodeAnalyzer(params: { * module path that is using it. */ const handleExpression = () => { - if (!isInMiddlewareLayer(parser)) { + if (!isInMiddlewareLayer(parser) || allowDynamicCodeEvaluation()) { return } @@ -364,7 +400,7 @@ function getCodeAnalyzer(params: { return } - if (dev) { + if (dev && !allowDynamicCodeEvaluation()) { const { ConstDependency } = wp.dependencies const dep1 = new ConstDependency( '__next_eval__(function() { return ', @@ -391,7 +427,7 @@ function getCodeAnalyzer(params: { return } - if (dev) { + if (dev && !allowDynamicCodeEvaluation()) { const { ConstDependency } = wp.dependencies const dep1 = new ConstDependency( '__next_webassembly_compile__(function() { return ', @@ -421,7 +457,7 @@ function getCodeAnalyzer(params: { return } - if (dev) { + if (dev && !allowDynamicCodeEvaluation()) { const { ConstDependency } = wp.dependencies const dep1 = new ConstDependency( '__next_webassembly_instantiate__(function() { return ', @@ -644,7 +680,6 @@ function getExtractMetadata(params: { 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 ${ @@ -715,7 +750,6 @@ function getExtractMetadata(params: { } } } - export default class MiddlewarePlugin { private readonly dev: boolean private readonly sriEnabled: boolean diff --git a/packages/next/server/dev/hot-reloader.ts b/packages/next/server/dev/hot-reloader.ts index 0130ed5a93ff..c0b2ea637e37 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/test/integration/edge-runtime-configurable-guards/lib/index.js b/test/integration/edge-runtime-configurable-guards/lib/index.js new file mode 100644 index 000000000000..8fa47bde2f62 --- /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 000000000000..361c04d84d89 --- /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 000000000000..9d808b1a2bb6 --- /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 000000000000..c5cc676685b6 --- /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 000000000000..284b4cf3a24f --- /dev/null +++ b/test/integration/edge-runtime-configurable-guards/test/index.test.js @@ -0,0 +1,302 @@ +/* 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 = '/' + +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.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 dynamic code', ({ init, url }) => { + beforeEach(() => init()) + + it('does not warn 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).not.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, + }) + expect(output.stderr).not.toContain(`Build failed`) + 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 with 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, + }) + expect(output.stderr).toContain(`Build failed`) + expect(output.stderr).not.toContain( + `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + ) + }) + }) +}) 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 000000000000..bd7d00f62c2c --- /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( + `exported 'config.allowDynamic' must be a string or an array of strings` + ) + }) +}) diff --git a/test/readme.md b/test/readme.md index 574c5fda5124..b9c28639e55f 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 From 24a2d2fbd3d4c6c9561d0ca79ddd878f349924e6 Mon Sep 17 00:00:00 2001 From: feugy Date: Wed, 17 Aug 2022 18:28:44 +0200 Subject: [PATCH 02/10] chore: leverages webpack ModuleGraph --- .../webpack/plugins/middleware-plugin.ts | 9 ++- .../test/index.test.js | 61 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 46b24005b7c2..e83ed927a75c 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -244,14 +244,17 @@ function isNodeJsModule(moduleName: string) { return require('module').builtinModules.includes(moduleName) } -function getEdgeEntryBuildInfo(module: webpack.Module) { +function getEdgeEntryBuildInfo( + moduleGraph: webpack.ModuleGraph, + module: webpack.Module +) { let currentModule: webpack.Module | null = module while (currentModule) { const buildInfo = getModuleBuildInfo(currentModule) if (buildInfo.edgeFunctionConfig) { return buildInfo } - currentModule = currentModule.issuer + currentModule = moduleGraph.getIssuer(currentModule) } } @@ -357,7 +360,7 @@ function getCodeAnalyzer(params: { function allowDynamicCodeEvaluation() { return isDynamicCodeEvaluationAllowed( parser.state.module.resource, - getEdgeEntryBuildInfo(parser.state.current) + getEdgeEntryBuildInfo(compilation.moduleGraph, parser.state.current) ) } diff --git a/test/integration/edge-runtime-configurable-guards/test/index.test.js b/test/integration/edge-runtime-configurable-guards/test/index.test.js index 284b4cf3a24f..42b235c3b935 100644 --- a/test/integration/edge-runtime-configurable-guards/test/index.test.js +++ b/test/integration/edge-runtime-configurable-guards/test/index.test.js @@ -52,6 +52,65 @@ describe('Edge runtime configurable guards', () => { 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('does not warn 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).not.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, + }) + 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`) + }) + }) + describe.each([ { title: 'Edge API', @@ -75,7 +134,7 @@ describe('Edge runtime configurable guards', () => { init() { context.middleware.write(` import { NextResponse } from 'next/server' - + export default () => { eval('100') return NextResponse.next() From 050c9e54daf85b33f5ab2ed85d65d49e7b9cf76c Mon Sep 17 00:00:00 2001 From: feugy Date: Thu, 18 Aug 2022 14:56:02 +0200 Subject: [PATCH 03/10] chore: speeds up edge config lookup --- packages/next/build/webpack/plugins/middleware-plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index e83ed927a75c..18828a849293 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -249,7 +249,7 @@ function getEdgeEntryBuildInfo( module: webpack.Module ) { let currentModule: webpack.Module | null = module - while (currentModule) { + while (currentModule?.layer === 'middleware') { const buildInfo = getModuleBuildInfo(currentModule) if (buildInfo.edgeFunctionConfig) { return buildInfo From 438fad02eddd55bf59683f6d1e00ab9754b82c28 Mon Sep 17 00:00:00 2001 From: feugy Date: Tue, 23 Aug 2022 15:43:12 +0200 Subject: [PATCH 04/10] refactor: uses the appropriate webpack hooks --- .../webpack/loaders/get-module-build-info.ts | 2 - .../loaders/next-edge-function-loader.ts | 7 +- .../webpack/loaders/next-middleware-loader.ts | 7 +- .../webpack/plugins/middleware-plugin.ts | 210 ++++++++++-------- .../test/index.test.js | 36 ++- 5 files changed, 145 insertions(+), 117 deletions(-) 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 53ccc85c7b34..848399cd32a0 100644 --- a/packages/next/build/webpack/loaders/get-module-build-info.ts +++ b/packages/next/build/webpack/loaders/get-module-build-info.ts @@ -1,6 +1,5 @@ import type { MiddlewareMatcher } from '../../analysis/get-page-static-info' import { webpack } from 'next/dist/compiled/webpack/webpack' -import type { MiddlewareConfig } from '../../analysis/get-page-static-info' /** * A getter for module build info that casts to the type it should have. @@ -17,7 +16,6 @@ export function getModuleBuildInfo(webpackModule: webpack.Module) { usingIndirectEval?: boolean | Set route?: RouteMeta importLocByPath?: Map - edgeFunctionConfig?: Partial 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 1e64f95edfd8..ba56808f47c4 100644 --- a/packages/next/build/webpack/loaders/next-edge-function-loader.ts +++ b/packages/next/build/webpack/loaders/next-edge-function-loader.ts @@ -1,6 +1,5 @@ import { getModuleBuildInfo } from './get-module-build-info' import { stringifyRequest } from '../stringify-request' -import { loadEdgeFunctionConfigFromFile } from './utils' export type EdgeFunctionLoaderOptions = { absolutePagePath: string @@ -8,7 +7,7 @@ export type EdgeFunctionLoaderOptions = { rootDir: string } -export default async function middlewareLoader(this: any) { +export default function middlewareLoader(this: any) { const { absolutePagePath, page, rootDir }: EdgeFunctionLoaderOptions = this.getOptions() const stringifiedPagePath = stringifyRequest(this, absolutePagePath) @@ -16,10 +15,6 @@ export default async function middlewareLoader(this: any) { buildInfo.nextEdgeApiFunction = { page: page || '/', } - buildInfo.edgeFunctionConfig = await loadEdgeFunctionConfigFromFile( - absolutePagePath, - this.getResolve() - ) buildInfo.rootDir = rootDir return ` diff --git a/packages/next/build/webpack/loaders/next-middleware-loader.ts b/packages/next/build/webpack/loaders/next-middleware-loader.ts index 8f1c41e2c7a0..a7f5e40420d2 100644 --- a/packages/next/build/webpack/loaders/next-middleware-loader.ts +++ b/packages/next/build/webpack/loaders/next-middleware-loader.ts @@ -2,7 +2,6 @@ import type { MiddlewareMatcher } from '../../analysis/get-page-static-info' import { getModuleBuildInfo } from './get-module-build-info' import { stringifyRequest } from '../stringify-request' import { MIDDLEWARE_LOCATION_REGEXP } from '../../../lib/constants' -import { loadEdgeFunctionConfigFromFile } from './utils' export type MiddlewareLoaderOptions = { absolutePagePath: string @@ -23,7 +22,7 @@ export function decodeMatchers(encodedMatchers: string) { ) as MiddlewareMatcher[] } -export default async function middlewareLoader(this: any) { +export default function middlewareLoader(this: any) { const { absolutePagePath, page, @@ -38,10 +37,6 @@ export default async function middlewareLoader(this: any) { page: page.replace(new RegExp(`/${MIDDLEWARE_LOCATION_REGEXP}$`), '') || '/', } - buildInfo.edgeFunctionConfig = await loadEdgeFunctionConfigFromFile( - absolutePagePath, - this.getResolve() - ) buildInfo.rootDir = rootDir return ` diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 18828a849293..7e79f0b7530e 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -20,7 +20,10 @@ import { FLIGHT_SERVER_CSS_MANIFEST, SUBRESOURCE_INTEGRITY_MANIFEST, } from '../../../shared/lib/constants' -import type { MiddlewareConfig } from '../../analysis/get-page-static-info' +import { + getPageStaticInfo, + MiddlewareConfig, +} from '../../analysis/get-page-static-info' export interface EdgeFunctionDefinition { env: string[] @@ -56,18 +59,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 @@ -244,29 +247,13 @@ function isNodeJsModule(moduleName: string) { return require('module').builtinModules.includes(moduleName) } -function getEdgeEntryBuildInfo( - moduleGraph: webpack.ModuleGraph, - module: webpack.Module -) { - let currentModule: webpack.Module | null = module - while (currentModule?.layer === 'middleware') { - const buildInfo = getModuleBuildInfo(currentModule) - if (buildInfo.edgeFunctionConfig) { - return buildInfo - } - currentModule = moduleGraph.getIssuer(currentModule) - } -} - function isDynamicCodeEvaluationAllowed( fileName: string, - buildInfo?: { - edgeFunctionConfig?: Partial - rootDir?: string - } + edgeFunctionConfig?: Partial, + rootDir?: string ) { - const name = fileName.replace(buildInfo?.rootDir ?? '', '') - for (const glob of buildInfo?.edgeFunctionConfig?.allowDynamicGlobs ?? []) { + const name = fileName.replace(rootDir ?? '', '') + for (const glob of edgeFunctionConfig?.allowDynamicGlobs ?? []) { if (isMatch(name, glob)) { return true } @@ -357,13 +344,6 @@ function getCodeAnalyzer(params: { } = params const { hooks } = parser - function allowDynamicCodeEvaluation() { - return isDynamicCodeEvaluationAllowed( - parser.state.module.resource, - getEdgeEntryBuildInfo(compilation.moduleGraph, parser.state.current) - ) - } - /** * 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 @@ -371,7 +351,7 @@ function getCodeAnalyzer(params: { * module path that is using it. */ const handleExpression = () => { - if (!isInMiddlewareLayer(parser) || allowDynamicCodeEvaluation()) { + if (!isInMiddlewareLayer(parser)) { return } @@ -403,18 +383,16 @@ function getCodeAnalyzer(params: { return } - if (dev && !allowDynamicCodeEvaluation()) { - 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 @@ -430,18 +408,16 @@ function getCodeAnalyzer(params: { return } - if (dev && !allowDynamicCodeEvaluation()) { - 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() } @@ -460,18 +436,16 @@ function getCodeAnalyzer(params: { return } - if (dev && !allowDynamicCodeEvaluation()) { - 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) } /** @@ -625,6 +599,32 @@ 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 ( + await getPageStaticInfo({ + nextConfig: {}, + pageFilePath, + isDev: false, + }) + ).middleware + } + } +} + function getExtractMetadata(params: { compilation: webpack.Compilation compiler: webpack.Compiler @@ -633,26 +633,35 @@ function getExtractMetadata(params: { }) { const { dev, compilation, metadataByEntry, compiler } = params const { webpack: wp } = compiler - return () => { + return async () => { metadataByEntry.clear() + const resolver = compilation.resolverFactory.get('normal') - 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(), @@ -660,8 +669,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` @@ -672,30 +681,39 @@ 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 ( + !isDynamicCodeEvaluationAllowed( + module.userRequest, + edgeFunctionConfig, + 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( + ', ' + )}` + : '' + }`, + entryModule: module, + compilation, + }) + ) + } } /** @@ -742,9 +760,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) } } } @@ -781,7 +799,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/test/integration/edge-runtime-configurable-guards/test/index.test.js b/test/integration/edge-runtime-configurable-guards/test/index.test.js index 42b235c3b935..3987b354c9fe 100644 --- a/test/integration/edge-runtime-configurable-guards/test/index.test.js +++ b/test/integration/edge-runtime-configurable-guards/test/index.test.js @@ -77,12 +77,12 @@ describe('Edge runtime configurable guards', () => { `) }) - it('does not warn in dev for allowed code', async () => { + 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).not.toContain( + expect(context.logs.output).toContain( `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` ) }) @@ -191,15 +191,15 @@ describe('Edge runtime configurable guards', () => { `) }, }, - ])('$title with allowed dynamic code', ({ init, url }) => { + ])('$title with allowed, used dynamic code', ({ init, url }) => { beforeEach(() => init()) - it('does not warn in dev at runtime', async () => { + 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).not.toContain( + expect(context.logs.output).toContain( `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` ) }) @@ -292,7 +292,7 @@ describe('Edge runtime configurable guards', () => { `) }, }, - ])('$title with allowed unused dynamic code', ({ init, url }) => { + ])('$title with allowed, unused dynamic code', ({ init, url }) => { beforeEach(() => init()) it('build and does not warn at runtime', async () => { @@ -334,7 +334,29 @@ describe('Edge runtime configurable guards', () => { `) }, }, - ])('$title with dynamic code', ({ init, url }) => { + { + 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 () => { From 112681d21ae4331925b8bab8829a8ef570b9e054 Mon Sep 17 00:00:00 2001 From: feugy Date: Mon, 29 Aug 2022 15:57:58 +0200 Subject: [PATCH 05/10] feat(middleware): adds telemetry for allowDynamic configuration --- .../webpack/plugins/middleware-plugin.ts | 37 ++++++++++++++----- .../test/index.test.js | 11 +++++- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index 7e79f0b7530e..d1974a53b361 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -24,6 +24,8 @@ import { getPageStaticInfo, MiddlewareConfig, } from '../../analysis/get-page-static-info' +import { Telemetry } from '../../../telemetry/storage' +import { traceGlobals } from '../../../trace/shared' export interface EdgeFunctionDefinition { env: string[] @@ -614,13 +616,16 @@ async function findEntryEdgeFunctionConfig( ) ) if (typeof pageFilePath === 'string') { - return ( - await getPageStaticInfo({ - nextConfig: {}, - pageFilePath, - isDev: false, - }) - ).middleware + return { + file: pageFilePath, + config: ( + await getPageStaticInfo({ + nextConfig: {}, + pageFilePath, + isDev: false, + }) + ).middleware, + } } } } @@ -636,6 +641,7 @@ function getExtractMetadata(params: { return async () => { metadataByEntry.clear() const resolver = compilation.resolverFactory.get('normal') + const telemetry: Telemetry = traceGlobals.get('telemetry') for (const [entryName, entry] of compilation.entries) { if (entry.options.runtime !== EDGE_RUNTIME_WEBPACK) { @@ -693,10 +699,23 @@ function getExtractMetadata(params: { continue } + 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, + edgeFunctionConfig?.config, rootDir ) ) { @@ -708,7 +727,7 @@ function getExtractMetadata(params: { ', ' )}` : '' - }`, + }\nLearn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`, entryModule: module, compilation, }) diff --git a/test/integration/edge-runtime-configurable-guards/test/index.test.js b/test/integration/edge-runtime-configurable-guards/test/index.test.js index 3987b354c9fe..7d72ba70111a 100644 --- a/test/integration/edge-runtime-configurable-guards/test/index.test.js +++ b/test/integration/edge-runtime-configurable-guards/test/index.test.js @@ -35,6 +35,7 @@ const appOption = { } const routeUrl = '/api/route' const middlewareUrl = '/' +const TELEMETRY_EVENT_NAME = 'NEXT_EDGE_ALLOW_DYNAMIC_USED' describe('Edge runtime configurable guards', () => { beforeEach(async () => { @@ -101,6 +102,7 @@ describe('Edge runtime configurable guards', () => { 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`) @@ -108,6 +110,7 @@ describe('Edge runtime configurable guards', () => { `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) }) }) @@ -299,8 +302,10 @@ describe('Edge runtime configurable guards', () => { 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) @@ -373,11 +378,13 @@ describe('Edge runtime configurable guards', () => { 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).not.toContain( - `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` + 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) }) }) }) From eb2c584c98d7a4ccb7bda9464da3f821010bdb1c Mon Sep 17 00:00:00 2001 From: feugy Date: Mon, 29 Aug 2022 15:58:52 +0200 Subject: [PATCH 06/10] docs(middleware): single error page for dynamic errors, documents config.allowDynamic key. --- docs/api-reference/edge-runtime.md | 19 +++++++++++ errors/edge-dynamic-code-evaluation.md | 34 +++++++++++++++++++ errors/manifest.json | 6 +++- errors/middleware-dynamic-wasm-compilation.md | 27 --------------- packages/next/server/web/sandbox/context.ts | 7 ++-- 5 files changed, 62 insertions(+), 31 deletions(-) create mode 100644 errors/edge-dynamic-code-evaluation.md delete mode 100644 errors/middleware-dynamic-wasm-compilation.md diff --git a/docs/api-reference/edge-runtime.md b/docs/api-reference/edge-runtime.md index c62a84973bf4..6d0b7f0c8395 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 threeshaking. +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 fail your route_. ## Related diff --git a/errors/edge-dynamic-code-evaluation.md b/errors/edge-dynamic-code-evaluation.md new file mode 100644 index 000000000000..a97dd0e85e85 --- /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 threeshaking. +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 fail your route_. diff --git a/errors/manifest.json b/errors/manifest.json index 42134b6ca26f..872dcea5b8a6 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -712,7 +712,11 @@ }, { "title": "middleware-dynamic-wasm-compilation", - "path": "/errors/middleware-dynamic-wasm-compilation.md" + "path": "/errors/edge-dynamic-code-evaluation.md" + }, + { + "title": "edge-dynamic-code-evaluation", + "path": "/errors/edge-dynamic-code-evaluation.md" }, { "title": "node-module-in-edge-runtime", diff --git a/errors/middleware-dynamic-wasm-compilation.md b/errors/middleware-dynamic-wasm-compilation.md deleted file mode 100644 index 7b5272a41d50..000000000000 --- a/errors/middleware-dynamic-wasm-compilation.md +++ /dev/null @@ -1,27 +0,0 @@ -# Dynamic WASM compilation is not available in Middlewares - -#### Why This Error Occurred - -Compiling WASM binaries dynamically is not allowed in Middlewares. Specifically, -the following APIs are not supported: - -- `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 - -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 -} -``` diff --git a/packages/next/server/web/sandbox/context.ts b/packages/next/server/web/sandbox/context.ts index 65b076daa3ae..d905ddc7625f 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' From db810169ed9e8a9206e8e91909e97996ce4bbd17 Mon Sep 17 00:00:00 2001 From: feugy Date: Fri, 2 Sep 2022 09:19:30 +0200 Subject: [PATCH 07/10] chore: improves validation message --- packages/next/build/analysis/get-page-static-info.ts | 7 +++++-- packages/next/build/webpack/plugins/middleware-plugin.ts | 7 +------ test/production/edge-config-validations/index.test.ts | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/next/build/analysis/get-page-static-info.ts b/packages/next/build/analysis/get-page-static-info.ts index cb7c718c95c4..4c65fa8a2014 100644 --- a/packages/next/build/analysis/get-page-static-info.ts +++ b/packages/next/build/analysis/get-page-static-info.ts @@ -12,6 +12,7 @@ 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[] @@ -177,9 +178,11 @@ function getMiddlewareConfig( ? config.allowDynamic : [config.allowDynamic] for (const glob of result.allowDynamicGlobs ?? []) { - if (typeof glob !== 'string') { + try { + matcher(glob) + } catch (err) { throw new Error( - `A middleware/edge exported 'config.allowDynamic' must be a string or an array of strings` + `A middleware/edge exported 'config.allowDynamic' is not a valid pattern: ${err.message}` ) } } diff --git a/packages/next/build/webpack/plugins/middleware-plugin.ts b/packages/next/build/webpack/plugins/middleware-plugin.ts index d1974a53b361..cd841609ffb2 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -255,12 +255,7 @@ function isDynamicCodeEvaluationAllowed( rootDir?: string ) { const name = fileName.replace(rootDir ?? '', '') - for (const glob of edgeFunctionConfig?.allowDynamicGlobs ?? []) { - if (isMatch(name, glob)) { - return true - } - } - return false + return isMatch(name, edgeFunctionConfig?.allowDynamicGlobs ?? []) } function buildUnsupportedApiError({ diff --git a/test/production/edge-config-validations/index.test.ts b/test/production/edge-config-validations/index.test.ts index bd7d00f62c2c..8c4f059195e5 100644 --- a/test/production/edge-config-validations/index.test.ts +++ b/test/production/edge-config-validations/index.test.ts @@ -29,7 +29,7 @@ describe('Edge config validations', () => { }) await expect(next.start()).rejects.toThrow('next build failed') expect(next.cliOutput).toMatch( - `exported 'config.allowDynamic' must be a string or an array of strings` + `exported 'config.allowDynamic' is not a valid pattern: Expected pattern to be a non-empty string` ) }) }) From 75fc85d22df9d5745ecd374883125f00e186141d Mon Sep 17 00:00:00 2001 From: feugy Date: Mon, 5 Sep 2022 08:51:32 +0200 Subject: [PATCH 08/10] chore: fix typings --- packages/next/build/analysis/get-page-static-info.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/next/build/analysis/get-page-static-info.ts b/packages/next/build/analysis/get-page-static-info.ts index 4c65fa8a2014..6147cf4bebcd 100644 --- a/packages/next/build/analysis/get-page-static-info.ts +++ b/packages/next/build/analysis/get-page-static-info.ts @@ -182,7 +182,9 @@ function getMiddlewareConfig( matcher(glob) } catch (err) { throw new Error( - `A middleware/edge exported 'config.allowDynamic' is not a valid pattern: ${err.message}` + `A middleware/edge exported 'config.allowDynamic' is not a valid pattern: ${ + (err as Error).message + }` ) } } From d26017bdb38e8f595a619d18e674ea18d7d73719 Mon Sep 17 00:00:00 2001 From: feugy Date: Mon, 12 Sep 2022 14:04:35 -0700 Subject: [PATCH 09/10] chore: iterates on PR comments and improves error reporting --- docs/api-reference/edge-runtime.md | 4 +-- errors/edge-dynamic-code-evaluation.md | 4 +-- errors/middleware-dynamic-wasm-compilation.md | 25 +++++++++++++++++++ .../build/analysis/get-page-static-info.ts | 9 +++++-- .../edge-config-validations/index.test.ts | 2 +- 5 files changed, 37 insertions(+), 7 deletions(-) create mode 100644 errors/middleware-dynamic-wasm-compilation.md diff --git a/docs/api-reference/edge-runtime.md b/docs/api-reference/edge-runtime.md index 6d0b7f0c8395..7993c3a6e80f 100644 --- a/docs/api-reference/edge-runtime.md +++ b/docs/api-reference/edge-runtime.md @@ -139,7 +139,7 @@ The following JavaScript language features are disabled, and **will not work:** - `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 threeshaking. +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 @@ -154,7 +154,7 @@ export const config = { `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 fail your route_. +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 index a97dd0e85e85..29a5e74b6329 100644 --- a/errors/edge-dynamic-code-evaluation.md +++ b/errors/edge-dynamic-code-evaluation.md @@ -28,7 +28,7 @@ export default async function middleware() { } ``` -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 threeshaking. +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 fail your route_. +Be warned that if these statements are executed on the Edge, _they will throw and cause a runtime error_. diff --git a/errors/middleware-dynamic-wasm-compilation.md b/errors/middleware-dynamic-wasm-compilation.md new file mode 100644 index 000000000000..7604d85915a8 --- /dev/null +++ b/errors/middleware-dynamic-wasm-compilation.md @@ -0,0 +1,25 @@ +# Dynamic WASM compilation is not available in Middlewares + +#### Why This Error Occurred + +Compiling WASM binaries dynamically is not allowed in Middlewares. Specifically, +the following APIs are not supported: + +- `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 + +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 +} +``` diff --git a/packages/next/build/analysis/get-page-static-info.ts b/packages/next/build/analysis/get-page-static-info.ts index 6147cf4bebcd..8b6155a86fb5 100644 --- a/packages/next/build/analysis/get-page-static-info.ts +++ b/packages/next/build/analysis/get-page-static-info.ts @@ -164,6 +164,7 @@ function getMiddlewareMatchers( } function getMiddlewareConfig( + pageFilePath: string, config: any, nextConfig: NextConfig ): Partial { @@ -182,7 +183,7 @@ function getMiddlewareConfig( matcher(glob) } catch (err) { throw new Error( - `A middleware/edge exported 'config.allowDynamic' is not a valid pattern: ${ + `${pageFilePath} exported 'config.allowDynamic' contains invalid pattern '${glob}': ${ (err as Error).message }` ) @@ -291,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/test/production/edge-config-validations/index.test.ts b/test/production/edge-config-validations/index.test.ts index 8c4f059195e5..ef0df8f55d00 100644 --- a/test/production/edge-config-validations/index.test.ts +++ b/test/production/edge-config-validations/index.test.ts @@ -29,7 +29,7 @@ describe('Edge config validations', () => { }) await expect(next.start()).rejects.toThrow('next build failed') expect(next.cliOutput).toMatch( - `exported 'config.allowDynamic' is not a valid pattern: Expected pattern to be a non-empty string` + `/middleware exported 'config.allowDynamic' contains invalid pattern 'true': Expected pattern to be a non-empty string` ) }) }) From 0b2b4a67081cc87207fb8a03eb2f3415f13a8815 Mon Sep 17 00:00:00 2001 From: feugy Date: Mon, 12 Sep 2022 14:34:39 -0700 Subject: [PATCH 10/10] chore: restores previous version middleware wasm error page --- errors/manifest.json | 2 +- errors/middleware-dynamic-wasm-compilation.md | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/errors/manifest.json b/errors/manifest.json index 872dcea5b8a6..41bb35c7d600 100644 --- a/errors/manifest.json +++ b/errors/manifest.json @@ -712,7 +712,7 @@ }, { "title": "middleware-dynamic-wasm-compilation", - "path": "/errors/edge-dynamic-code-evaluation.md" + "path": "/errors/middleware-dynamic-wasm-compilation.md" }, { "title": "edge-dynamic-code-evaluation", diff --git a/errors/middleware-dynamic-wasm-compilation.md b/errors/middleware-dynamic-wasm-compilation.md index 7604d85915a8..ffe3b85546f4 100644 --- a/errors/middleware-dynamic-wasm-compilation.md +++ b/errors/middleware-dynamic-wasm-compilation.md @@ -15,10 +15,12 @@ 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 }