From 73be5cea9f3f08a09d47043ae9f485b3341d3733 Mon Sep 17 00:00:00 2001 From: feugy Date: Tue, 23 Aug 2022 15:43:12 +0200 Subject: [PATCH] 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 | 212 ++++++++++-------- .../test/index.test.js | 36 ++- 5 files changed, 146 insertions(+), 118 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 a9a1db4f3edc92b..795caa9c4df3041 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,4 @@ 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,7 +15,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 1e64f95edfd88ff..ba56808f47c4a48 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 e97c838383bb066..0ae1da74f8a2487 100644 --- a/packages/next/build/webpack/loaders/next-middleware-loader.ts +++ b/packages/next/build/webpack/loaders/next-middleware-loader.ts @@ -1,7 +1,6 @@ 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 @@ -10,7 +9,7 @@ export type MiddlewareLoaderOptions = { rootDir: string } -export default async function middlewareLoader(this: any) { +export default function middlewareLoader(this: any) { const { absolutePagePath, page, @@ -28,10 +27,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 087f64288900a33..3804d338ae5fc0b 100644 --- a/packages/next/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/build/webpack/plugins/middleware-plugin.ts @@ -17,7 +17,10 @@ import { MIDDLEWARE_REACT_LOADABLE_MANIFEST, NEXT_CLIENT_SSR_ENTRY_SUFFIX, } 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[] @@ -59,18 +62,18 @@ const middlewareManifest: MiddlewareManifest = { * 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 @@ -230,29 +233,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 } @@ -343,13 +330,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 @@ -357,7 +337,7 @@ function getCodeAnalyzer(params: { * module path that is using it. */ const handleExpression = () => { - if (!isInMiddlewareLayer(parser) || allowDynamicCodeEvaluation()) { + if (!isInMiddlewareLayer(parser)) { return } @@ -389,18 +369,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 @@ -416,18 +394,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() } @@ -446,18 +422,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) } /** @@ -611,6 +585,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 @@ -619,26 +619,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(), @@ -646,8 +655,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` @@ -658,30 +667,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, + }) + ) + } } /** @@ -728,9 +746,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) } } } @@ -747,7 +765,7 @@ export default class MiddlewarePlugin { } apply(compiler: webpack.Compiler) { - compiler.hooks.compilation.tap(NAME, async (compilation, params) => { + compiler.hooks.compilation.tap(NAME, (compilation, params) => { const { hooks } = params.normalModuleFactory /** @@ -766,7 +784,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 42b235c3b9350df..3987b354c9febfc 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 () => {