Skip to content

Commit

Permalink
Fix unstable_allowDynamic when used with pnpm (#73732)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
unstubbable authored Dec 10, 2024
1 parent 4fdc2ac commit 7db3bf7
Show file tree
Hide file tree
Showing 7 changed files with 51 additions and 23 deletions.
2 changes: 1 addition & 1 deletion docs/01-app/03-api-reference/07-edge.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,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/**',
],
}
```
Expand Down
2 changes: 1 addition & 1 deletion errors/edge-dynamic-code-evaluation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ You can relax the check to allow specific files with your Middleware [configurat
export const config = {
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
],
}
```
Expand Down
4 changes: 3 additions & 1 deletion packages/next/src/build/webpack/plugins/middleware-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,9 @@ function isDynamicCodeEvaluationAllowed(

const name = fileName.replace(rootDir ?? '', '')

return picomatch(middlewareConfig?.unstable_allowDynamic ?? [])(name)
return picomatch(middlewareConfig?.unstable_allowDynamic ?? [], {
dot: true,
})(name)
}

function buildUnsupportedApiError({
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -167,14 +173,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(`
Expand All @@ -190,15 +196,15 @@ 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 () {
await hasDynamic()
return NextResponse.next()
}
export const config = {
unstable_allowDynamic: '/lib/**'
unstable_allowDynamic: '**/node_modules/lib/**'
}
`)
context.lib.write(`
Expand All @@ -207,15 +213,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`
)
Expand Down Expand Up @@ -265,14 +275,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(`
Expand All @@ -290,14 +300,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(`
Expand Down Expand Up @@ -356,7 +366,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 })
Expand All @@ -372,14 +382,17 @@ 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',
url: middlewareUrl,
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()
Expand All @@ -394,20 +407,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 () => {
Expand Down Expand Up @@ -446,7 +463,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()
Expand Down

0 comments on commit 7db3bf7

Please sign in to comment.