From 030e73ab00bf594f41800ac0adb4f09a3f1e1406 Mon Sep 17 00:00:00 2001 From: Hendrik Liebau Date: Tue, 10 Dec 2024 22:14:40 +0100 Subject: [PATCH] Backport: Fix `unstable_allowDynamic` when used with pnpm When using dependencies in Middleware that make use of dynamic code evaluation, Next.js emits a build error because this is not supported in the Edge runtime. In rare cases, when the code can not be reached at runtime and can't be removed by tree-shaking, users might opt in to using the `unstable_allowDynamic` config. When combined with pnpm, the provided glob patterns as documented at https://nextjs.org/docs/messages/edge-dynamic-code-evaluation#possible-ways-to-fix-it did not match correctly because of pnpm's use of the `.pnpm` directory. To fix the pattern matching, we need to provide the `dot` option to [picomatch](https://github.com/micromatch/picomatch), which enables dotfile matching. _Side note: Ideally we would detect dynamic code evaluation after tree shaking, to reduce the number of cases where users need to revert to using `unstable_allowDynamic`._ fixes #51401 --- docs/02-app/02-api-reference/07-edge.mdx | 2 +- errors/edge-dynamic-code-evaluation.mdx | 2 +- .../build/analysis/get-page-static-info.ts | 2 +- .../webpack/plugins/middleware-plugin.ts | 4 +- .../.pnpm/test/node_modules}/lib/index.js | 0 .../.pnpm/test/node_modules/lib/package.json | 8 +++ .../node_modules/lib | 1 + .../test/index.test.js | 60 ++++++++++++------- 8 files changed, 55 insertions(+), 24 deletions(-) rename test/integration/edge-runtime-configurable-guards/{ => node_modules/.pnpm/test/node_modules}/lib/index.js (100%) create mode 100644 test/integration/edge-runtime-configurable-guards/node_modules/.pnpm/test/node_modules/lib/package.json create mode 120000 test/integration/edge-runtime-configurable-guards/node_modules/lib diff --git a/docs/02-app/02-api-reference/07-edge.mdx b/docs/02-app/02-api-reference/07-edge.mdx index 3bed7bfb2bbb0e..4a25ad53f26305 100644 --- a/docs/02-app/02-api-reference/07-edge.mdx +++ b/docs/02-app/02-api-reference/07-edge.mdx @@ -155,7 +155,7 @@ export const config = { // allows a single file '/lib/utilities.js', // use a glob to allow anything in the function-bind 3rd party module - '/node_modules/function-bind/**', + '**/node_modules/function-bind/**', ], } ``` diff --git a/errors/edge-dynamic-code-evaluation.mdx b/errors/edge-dynamic-code-evaluation.mdx index bb842f4181ddd4..fcf7bcdc79b732 100644 --- a/errors/edge-dynamic-code-evaluation.mdx +++ b/errors/edge-dynamic-code-evaluation.mdx @@ -38,7 +38,7 @@ export const config = { runtime: 'edge', // for Edge API Routes only unstable_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 + '**/node_modules/function-bind/**', // use a glob to allow anything in the function-bind 3rd party module ], } ``` diff --git a/packages/next/src/build/analysis/get-page-static-info.ts b/packages/next/src/build/analysis/get-page-static-info.ts index 4420c266144c38..590fcd855ac1f5 100644 --- a/packages/next/src/build/analysis/get-page-static-info.ts +++ b/packages/next/src/build/analysis/get-page-static-info.ts @@ -409,7 +409,7 @@ function getMiddlewareConfig( : [config.unstable_allowDynamic] for (const glob of result.unstable_allowDynamicGlobs ?? []) { try { - picomatch(glob) + picomatch(glob, { dot: true }) } catch (err) { throw new Error( `${pageFilePath} exported 'config.unstable_allowDynamic' contains invalid pattern '${glob}': ${ diff --git a/packages/next/src/build/webpack/plugins/middleware-plugin.ts b/packages/next/src/build/webpack/plugins/middleware-plugin.ts index f0a6492f6b2f8f..5d0fd0e977cc0f 100644 --- a/packages/next/src/build/webpack/plugins/middleware-plugin.ts +++ b/packages/next/src/build/webpack/plugins/middleware-plugin.ts @@ -291,7 +291,9 @@ function isDynamicCodeEvaluationAllowed( const name = fileName.replace(rootDir ?? '', '') - return picomatch(middlewareConfig?.unstable_allowDynamicGlobs ?? [])(name) + return picomatch(middlewareConfig?.unstable_allowDynamicGlobs ?? [], { + dot: true, + })(name) } function buildUnsupportedApiError({ diff --git a/test/integration/edge-runtime-configurable-guards/lib/index.js b/test/integration/edge-runtime-configurable-guards/node_modules/.pnpm/test/node_modules/lib/index.js similarity index 100% rename from test/integration/edge-runtime-configurable-guards/lib/index.js rename to test/integration/edge-runtime-configurable-guards/node_modules/.pnpm/test/node_modules/lib/index.js diff --git a/test/integration/edge-runtime-configurable-guards/node_modules/.pnpm/test/node_modules/lib/package.json b/test/integration/edge-runtime-configurable-guards/node_modules/.pnpm/test/node_modules/lib/package.json new file mode 100644 index 00000000000000..31bc38ec509ab1 --- /dev/null +++ b/test/integration/edge-runtime-configurable-guards/node_modules/.pnpm/test/node_modules/lib/package.json @@ -0,0 +1,8 @@ +{ + "name": "lib", + "private": true, + "type": "module", + "exports": { + ".": "./index.js" + } +} diff --git a/test/integration/edge-runtime-configurable-guards/node_modules/lib b/test/integration/edge-runtime-configurable-guards/node_modules/lib new file mode 120000 index 00000000000000..ad4b31694ebd68 --- /dev/null +++ b/test/integration/edge-runtime-configurable-guards/node_modules/lib @@ -0,0 +1 @@ +.pnpm/test/node_modules/lib \ No newline at end of file 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 9fbd1a1a163cfa..bd979ae2168215 100644 --- a/test/integration/edge-runtime-configurable-guards/test/index.test.js +++ b/test/integration/edge-runtime-configurable-guards/test/index.test.js @@ -21,7 +21,13 @@ const context = { 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')), + lib: new File( + join( + __dirname, + // Simulated .pnpm node_modules path: + '../node_modules/.pnpm/test/node_modules/lib/index.js' + ) + ), } const appOption = { env: { __NEXT_TEST_WITH_DEVTOOL: 1 }, @@ -74,7 +80,7 @@ describe('Edge runtime configurable guards', () => { } export const config = { runtime: 'edge', - unstable_allowDynamic: '/lib/**' + unstable_allowDynamic: '**/node_modules/lib/**' } `) await waitFor(500) @@ -162,14 +168,14 @@ describe('Edge runtime configurable guards', () => { url: routeUrl, init() { context.api.write(` - import { hasDynamic } from '../../lib' + import { hasDynamic } from 'lib' export default async function handler(request) { await hasDynamic() return Response.json({ result: true }) } export const config = { runtime: 'edge', - unstable_allowDynamic: '/lib/**' + unstable_allowDynamic: '**/node_modules/lib/**' } `) context.lib.write(` @@ -178,6 +184,9 @@ describe('Edge runtime configurable guards', () => { } `) }, + // TODO: Re-enable when Turbopack applies the middleware dynamic code + // evaluation transforms also to code in node_modules. + skip: Boolean(process.env.TURBOPACK), }, { title: 'Middleware using lib', @@ -185,7 +194,7 @@ describe('Edge runtime configurable guards', () => { init() { context.middleware.write(` import { NextResponse } from 'next/server' - import { hasDynamic } from './lib' + import { hasDynamic } from 'lib' // populated with tests export default async function () { @@ -193,7 +202,7 @@ describe('Edge runtime configurable guards', () => { return NextResponse.next() } export const config = { - unstable_allowDynamic: '/lib/**' + unstable_allowDynamic: '**/node_modules/lib/**' } `) context.lib.write(` @@ -202,15 +211,19 @@ describe('Edge runtime configurable guards', () => { } `) }, + // TODO: Re-enable when Turbopack applies the middleware dynamic code + // evaluation transforms also to code in node_modules. + skip: Boolean(process.env.TURBOPACK), }, - ])('$title with allowed, used dynamic code', ({ init, url }) => { + ])('$title with allowed, used dynamic code', ({ init, url, skip }) => { beforeEach(() => init()) - - it('still warns in dev at runtime', async () => { + ;(skip ? it.skip : 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) + // eslint-disable-next-line jest/no-standalone-expect expect(res.status).toBe(200) + // eslint-disable-next-line jest/no-standalone-expect expect(context.logs.output).toContain( `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` ) @@ -260,14 +273,14 @@ describe('Edge runtime configurable guards', () => { url: routeUrl, init() { context.api.write(` - import { hasUnusedDynamic } from '../../lib' + import { hasUnusedDynamic } from 'lib' export default async function handler(request) { await hasUnusedDynamic() return Response.json({ result: true }) } export const config = { runtime: 'edge', - unstable_allowDynamic: '/lib/**' + unstable_allowDynamic: '**/node_modules/lib/**' } `) context.lib.write(` @@ -285,14 +298,14 @@ describe('Edge runtime configurable guards', () => { init() { context.middleware.write(` import { NextResponse } from 'next/server' - import { hasUnusedDynamic } from './lib' + import { hasUnusedDynamic } from 'lib' // populated with tests export default async function () { await hasUnusedDynamic() return NextResponse.next() } export const config = { - unstable_allowDynamic: '/lib/**' + unstable_allowDynamic: '**/node_modules/lib/**' } `) context.lib.write(` @@ -340,7 +353,7 @@ describe('Edge runtime configurable guards', () => { url: routeUrl, init() { context.api.write(` - import { hasDynamic } from '../../lib' + import { hasDynamic } from 'lib' export default async function handler(request) { await hasDynamic() return Response.json({ result: true }) @@ -356,6 +369,9 @@ describe('Edge runtime configurable guards', () => { } `) }, + // TODO: Re-enable when Turbopack applies the edge runtime transforms also + // to code in node_modules. + skip: Boolean(process.env.TURBOPACK), }, { title: 'Middleware using lib', @@ -363,7 +379,7 @@ describe('Edge runtime configurable guards', () => { init() { context.middleware.write(` import { NextResponse } from 'next/server' - import { hasDynamic } from './lib' + import { hasDynamic } from 'lib' export default async function () { await hasDynamic() return NextResponse.next() @@ -378,20 +394,24 @@ describe('Edge runtime configurable guards', () => { } `) }, + // TODO: Re-enable when Turbopack applies the middleware dynamic code + // evaluation transforms also to code in node_modules. + skip: Boolean(process.env.TURBOPACK), }, - ])('$title with unallowed, used dynamic code', ({ init, url }) => { + ])('$title with unallowed, used dynamic code', ({ init, url, skip }) => { beforeEach(() => init()) - - it('warns in dev at runtime', async () => { + ;(skip ? it.skip : 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) + // eslint-disable-next-line jest/no-standalone-expect expect(res.status).toBe(200) + // eslint-disable-next-line jest/no-standalone-expect expect(context.logs.output).toContain( `Dynamic Code Evaluation (e. g. 'eval', 'new Function') not allowed in Edge Runtime` ) }) - ;(process.env.TURBOPACK_DEV ? describe.skip : describe)( + ;(skip || process.env.TURBOPACK_DEV ? describe.skip : describe)( 'production mode', () => { it('fails to build because of dynamic code evaluation', async () => { @@ -429,7 +449,7 @@ describe('Edge runtime configurable guards', () => { init() { context.middleware.write(` import { NextResponse } from 'next/server' - import { returnTrue } from './lib' + import { returnTrue } from 'lib' export default async function () { (() => {}) instanceof Function return NextResponse.next()