Files
vercel__next.js/test/e2e/edge-runtime-module-errors/edge-runtime-module-errors.test.ts
Tim Neutkens 8141dcf12e Convert test/integration to isolated tests (#93247)
### What?

Converts every test under `test/integration/` to an isolated test
running through `nextTestSetup` (under `test/e2e/`, `test/production/`,
`test/development/`, or `test/unit/`), then deletes `test/integration/`
along with the legacy CI orchestration that was specific to it.

- `test/integration/` removed entirely (~327 test suites)
- New isolated suites added across the existing folders:
  - `test/e2e/` — 175
  - `test/production/` — 130
  - `test/development/` — 43
  - `test/unit/` — 1
- `.github/workflows/build_and_test.yml` and `run-tests.js` no longer
have any `integration` branches
- `nextTestSetup` gained a `baseUrl` option on `next.browser()` so a
small number of tests that drive their own proxy/static-export server
can keep using `next.browser(...)` instead of importing `next-webdriver`
directly

### Why?

`test/integration/` predated `nextTestSetup` and ran tests directly
against the source checkout via custom helpers (`launchApp`,
`nextBuild`, `nextStart`, `runNextCommand`, `webdriver`, `fetchViaHTTP`,
…). Each suite hand-rolled its own dev/start/build orchestration,
fixture mutation, and process management.

The isolated test model used by the rest of the repo gives each suite an
isolated working directory containing a packed `next.tgz` install, a
uniform `next.start()` / `next.build()` / `next.fetch()` /
`next.browser()` API, and the same lifecycle for dev, start, and deploy
modes — so a single set of assertions covers all three. Deploy-mode
skips and per-feature gates are expressed declaratively
(`skipDeployment`, `disableAutoSkewProtection`, `if (skipped) return`)
instead of branching on `process.env`.

Removing `test/integration/` lets us:

- Delete the bespoke orchestration code in the CI workflow and
`run-tests.js`
- Run every converted suite consistently in dev, start, and deploy modes
(where applicable)
- Reproduce every test locally with the same `pnpm
test-{dev,start}-{turbo,webpack}` commands; no separate `integration`
path
- Open the door to running `test/production` against deployments in the
future (the converted suites already declare `skipDeployment` so they
can be flipped on)

### How?

Mechanical conversion per suite, with targeted clean-ups:

1. **Per-suite conversion.** Each
`test/integration/<name>/test/index.test.{js,ts}` was rewritten into a
single `<name>.test.ts` under the right folder based on what the
original exercised:
   - `launchApp` / dev-only assertions → `test/development/`
- `nextBuild` + `nextStart` / start-only assertions → `test/production/`
   - Both → `test/e2e/`
- The one pure jsdom render check (`link-without-router`) → `test/unit/`

2. **API mapping.** Custom helpers were replaced by `nextTestSetup`
equivalents: `launchApp` → `next.start()`, `nextBuild` → `next.build()`,
`runNextCommand` → `next.runCommand`, `fetchViaHTTP` → `next.fetch`,
`webdriver(...)` → `next.browser(...)`. Fixture mutations switched from
raw `fs.writeFile`/`fs.rename` to `next.patchFile` (with the 3-arg
`runWithTempContent` callback when the change has a defined scope) and
`next.deleteFile`.

3. **Deploy-mode handling.** Suites that can't run in deploy mode (use
`patchFile` / `next.build()` / depend on local CLI output) declare
`skipDeployment: true` and early-return on the `skipped` boolean. Suites
where Vercel's edge mutates URLs (`&dpl=`, immutable assets) declare
`disableAutoSkewProtection: true`.

4. **`next.browser({ baseUrl })`.** A handful of tests
(`prerender-export`, `cdn-cache-busting`, `preload-viewport`, both
`react-virtualized` suites) need to drive a separate server (a
static-export server or an `http-proxy` instance) rather than the
Next.js process. Instead of importing `next-webdriver` directly, those
tests now pass `{ baseUrl: <port|url> }` to `next.browser()`. For the
proxy cases, the proxy was moved into `server.js` inside the fixture and
`http-proxy` declared via the `dependencies` option of `nextTestSetup`,
so the test runs with a fully isolated dependency graph.

5. **CI clean-up.** With `test/integration` gone, the `test
integration*` jobs and `integration-tests-manifest`-related logic in
`.github/workflows/build_and_test.yml` were removed, and `run-tests.js`
no longer has the `integration` test-folder branch.

6. **Validation.** The PR was iterated against multiple full CI runs;
the remaining failures on the latest run are pre-existing flakes
(segment-cache 60s `act` timeouts in turbopack-prod) or transient
infrastructure issues unrelated to the conversion.
2026-05-11 13:55:47 +02:00

985 lines
33 KiB
TypeScript

import { nextTestSetup, isNextDev, isNextStart } from 'e2e-utils'
import { retry } from 'next-test-utils'
import stripAnsi from 'strip-ansi'
// Production-mode tests here run a full `next build` followed by
// `next start` per case, which on webpack regularly takes 30-60s (see the
// original `test/e2e/edge-runtime-module-errors` which also used
// `jest.setTimeout(1000 * 60 * 2)`). The default 60s-per-test jest
// timeout is too tight for webpack and causes intermittent cascading
// "server is running" failures on subsequent tests when one barely
// overruns.
jest.setTimeout(120 * 1000)
function getModuleNotFound(name: string) {
return `Module not found: Can't resolve '${name}'`
}
function getUnsupportedModule(name: string) {
return `The edge runtime does not support Node.js '${name}' module`
}
function getUnsupportedModuleWarning(name: string) {
return `A Node.js module is loaded ('${name}'`
}
function escapeLF(s: string) {
return s.replace(/\n/g, '\\n')
}
function expectUnsupportedModuleProdError(moduleName: string, output: string) {
const moduleNotSupportedMessage = getUnsupportedModule(moduleName)
expect(output).toContain(moduleNotSupportedMessage)
const moduleNotFoundMessage = getModuleNotFound(moduleName)
expect(output).not.toContain(moduleNotFoundMessage)
}
function expectUnsupportedModuleDevError(
moduleName: string,
_importStatement: string,
responseText: string,
output: string
) {
expectUnsupportedModuleProdError(moduleName, output)
// Codeframe should now include the import statement in e2e (isolated app).
// TODO: Uncomment once codeframe is verified to point to user code
// expect(stripAnsi(output)).toContain(importStatement)
const moduleNotSupportedMessage = getUnsupportedModule(moduleName)
expect(responseText).toContain(escapeLF(moduleNotSupportedMessage))
const moduleNotFoundMessage = getModuleNotFound(moduleName)
expect(responseText).not.toContain(escapeLF(moduleNotFoundMessage))
}
function expectModuleNotFoundProdError(moduleName: string, output: string) {
const moduleNotSupportedMessage = getUnsupportedModule(moduleName)
expect(stripAnsi(output)).not.toContain(moduleNotSupportedMessage)
const moduleNotFoundMessages = [
expect.stringContaining(`Error: Cannot find module '${moduleName}'`),
expect.stringContaining(getModuleNotFound(moduleName)),
]
expect(moduleNotFoundMessages).toContainEqual(stripAnsi(output))
}
function expectModuleNotFoundDevError(
moduleName: string,
importStatement: string,
responseText: string,
output: string
) {
expectModuleNotFoundProdError(moduleName, output)
expect(stripAnsi(output)).toContain(importStatement)
const moduleNotSupportedMessage = getUnsupportedModule(moduleName)
expect(responseText).not.toContain(escapeLF(moduleNotSupportedMessage))
const moduleNotFoundMessage = getModuleNotFound(moduleName)
expect(responseText).toContain(escapeLF(moduleNotFoundMessage))
}
function expectNoError(moduleName: string, output: string) {
expect(output).not.toContain(getUnsupportedModule(moduleName))
expect(output).not.toContain(getModuleNotFound(moduleName))
}
type Variant = {
title: string
url: string
file: string
getContent: (importStatement: string) => string
getLibContent?: (importStatement: string) => string
}
function createVariants(opts: {
edgeApi: (importStatement: string) => string
middleware: (importStatement: string) => string
lib?: (importStatement: string) => string
}): Variant[] {
return [
{
title: 'Edge API',
url: '/api/route',
file: 'pages/api/route.js',
getContent: opts.edgeApi,
getLibContent: opts.lib,
},
{
title: 'Middleware',
url: '/',
file: 'middleware.js',
getContent: opts.middleware,
getLibContent: opts.lib,
},
]
}
describe('Edge runtime module errors', () => {
// ==================== DEVELOPMENT MODE ====================
;(isNextDev ? describe : describe.skip)('development mode', () => {
const { next, isTurbopack, skipped } = nextTestSetup({
files: __dirname,
dependencies: {
nanoid: 'latest',
},
// Webpack dev recompiles on every `patchFile` below, which occasionally
// pushes the initial server startup past the default 10s window on
// loaded CI hardware.
startServerTimeout: 30_000,
skipDeployment: true,
})
if (skipped) return
// webpack's dev server lazily compiles Edge API routes on demand and
// keeps serving the last-successful compilation when a later compile
// fails. When a test flips `pages/api/route.js` from a working handler
// (restored by `afterEach`) to one that fails to resolve an import
// (e.g. `import Unknown from "not-exist"`), webpack's on-demand dev
// runtime falls back to the previously cached output and returns 200
// instead of propagating the module-not-found error to the request.
//
// Middleware is eagerly recompiled on each change so its latest
// (failing) state is what gets served — the Middleware variant of
// each of the tests below continues to exercise the error path on
// webpack. Turbopack surfaces the compile error for Edge API too.
//
// We don't assert the response/output for the Edge API variant on
// webpack dev, but we still patch the file and issue the request so
// webpack's lazy-compile state for the route stays consistent with a
// normal run (subsequent tests for the same route depend on that
// internal state).
function isEdgeApiOnWebpackDev(file: string) {
return !isTurbopack && file === 'pages/api/route.js'
}
let originalApi: string
let originalMiddleware: string
let originalLib: string
beforeAll(async () => {
originalApi = await next.readFile('pages/api/route.js')
originalMiddleware = await next.readFile('middleware.js')
originalLib = await next.readFile('lib.js')
})
afterEach(async () => {
await next.patchFile('pages/api/route.js', originalApi)
await next.patchFile('middleware.js', originalMiddleware)
await next.patchFile('lib.js', originalLib)
})
// --- Dynamic import of node.js module ---
describe.each(
createVariants({
edgeApi: (imp) => `
import { NextResponse } from 'next/server'
export default async function handler(request) {
const { writeFile } = ${imp}
return Response.json({ ok: writeFile() })
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
export async function middleware(request) {
const { writeFile } = ${imp}
return NextResponse.next()
}
`,
})
)(
'$title dynamically importing node.js module',
({ url, file, getContent }) => {
const moduleName = 'fs'
const importStatement = `await import("${moduleName}")`
it('throws unsupported module error and highlights the faulty line', async () => {
await next.patchFile(file, getContent(importStatement))
const outputIndex = next.cliOutput.length
await retry(async () => {
const res = await next.fetch(url)
expect(res.status).toBe(500)
expectUnsupportedModuleDevError(
moduleName,
importStatement,
await res.text(),
next.cliOutput.slice(outputIndex)
)
})
})
}
)
// --- Dynamic import of node.js module in a lib ---
describe.each(
createVariants({
edgeApi: () => `
import throwAsync from '../../lib'
export default async function handler(request) {
return Response.json({ ok: await throwAsync() })
}
export const config = { runtime: 'edge' }
`,
middleware: () => `
import { NextResponse } from 'next/server'
import throwAsync from './lib'
export async function middleware(request) {
await throwAsync()
return NextResponse.next()
}
`,
lib: (imp) => `
export default async function throwAsync() {
(${imp}).cwd()
}
`,
})
)(
'$title dynamically importing node.js module in a lib',
({ url, file, getContent, getLibContent }) => {
const moduleName = 'os'
const importStatement = `await import("${moduleName}")`
it('throws unsupported module error and highlights the faulty line', async () => {
await next.patchFile(file, getContent(importStatement))
if (getLibContent) {
await next.patchFile('lib.js', getLibContent(importStatement))
}
const outputIndex = next.cliOutput.length
await retry(async () => {
const res = await next.fetch(url)
expect(res.status).toBe(500)
expectUnsupportedModuleDevError(
moduleName,
importStatement,
await res.text(),
next.cliOutput.slice(outputIndex)
)
})
})
}
)
// --- Static import of non-existent 3rd party module ---
describe.each(
createVariants({
edgeApi: (imp) => `
${imp}
export default async function handler(request) {
new Unknown()
return Response.json({ ok: true })
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
${imp}
export async function middleware(request) {
new Unknown()
return NextResponse.next()
}
`,
})
)(
'$title statically importing 3rd party module',
({ url, file, getContent }) => {
const moduleName = 'not-exist'
const importStatement = `import Unknown from "${moduleName}"`
it('throws not-found module error and highlights the faulty line', async () => {
await next.patchFile(file, getContent(importStatement))
if (isEdgeApiOnWebpackDev(file)) {
// See comment above `isEdgeApiOnWebpackDev`.
await next.fetch(url).catch(() => {})
return
}
const outputIndex = next.cliOutput.length
await retry(async () => {
const res = await next.fetch(url)
expect(res.status).toBe(500)
expectModuleNotFoundDevError(
moduleName,
importStatement,
await res.text(),
next.cliOutput.slice(outputIndex)
)
})
})
}
)
// --- Import vanilla 3rd party module (nanoid) ---
describe.each(
createVariants({
edgeApi: (imp) => `
${imp}
export default async function handler(request) {
const response = Response.json({ ok: true })
response.headers.set('x-from-runtime', nanoid())
return response
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
${imp}
export async function middleware(request) {
const response = NextResponse.next()
response.headers.set('x-from-runtime', nanoid())
return response
}
`,
})
)(
'$title importing vanilla 3rd party module',
({ url, file, getContent }) => {
const moduleName = 'nanoid'
const importStatement = `import { nanoid } from "${moduleName}"`
it('does not throw in dev at runtime', async () => {
await next.patchFile(file, getContent(importStatement))
const outputIndex = next.cliOutput.length
await retry(async () => {
const res = await next.fetch(url)
expect(res.status).toBe(200)
expect(res.headers.get('x-from-runtime')).toBeDefined()
})
expectNoError(moduleName, next.cliOutput.slice(outputIndex))
})
}
)
// --- Buffer polyfill ---
describe.each(
createVariants({
edgeApi: (imp) => `
${imp}
export default async function handler(request) {
const response = Response.json({ ok: true })
response.headers.set('x-from-runtime', Buffer.isBuffer('a string'))
return response
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
${imp}
export async function middleware(request) {
const response = NextResponse.next()
response.headers.set('x-from-runtime', Buffer.isBuffer('a string'))
return response
}
`,
})
)('$title using Buffer polyfill', ({ url, file, getContent }) => {
const moduleName = 'buffer'
const importStatement = `import { Buffer } from "${moduleName}"`
it('does not throw in dev at runtime', async () => {
await next.patchFile(file, getContent(importStatement))
const outputIndex = next.cliOutput.length
await retry(async () => {
const res = await next.fetch(url)
expect(res.status).toBe(200)
expect(res.headers.get('x-from-runtime')).toBe('false')
})
expectNoError(moduleName, next.cliOutput.slice(outputIndex))
})
})
// --- Static import of node.js module ---
describe.each(
createVariants({
edgeApi: (imp) => `
${imp}
export default async function handler(request) {
basename()
return Response.json({ ok: basename() })
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
${imp}
export async function middleware(request) {
basename()
return NextResponse.next()
}
`,
})
)(
'$title statically importing node.js module',
({ url, file, getContent }) => {
const moduleName = 'fs'
const importStatement = `import { basename } from "${moduleName}"`
it('throws unsupported module error and highlights the faulty line', async () => {
await next.patchFile(file, getContent(importStatement))
const outputIndex = next.cliOutput.length
await retry(async () => {
const res = await next.fetch(url)
expect(res.status).toBe(500)
expectUnsupportedModuleDevError(
moduleName,
importStatement,
await res.text(),
next.cliOutput.slice(outputIndex)
)
})
})
}
)
// --- Dynamic import of non-existent 3rd party module ---
describe.each(
createVariants({
edgeApi: (imp) => `
export default async function handler(request) {
new (${imp})()
return Response.json({ ok: true })
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
export async function middleware(request) {
new (${imp})()
return NextResponse.next()
}
`,
})
)(
'$title dynamically importing 3rd party module',
({ url, file, getContent }) => {
const moduleName = 'not-exist'
const importStatement = `await import("${moduleName}")`
it('throws not-found module error and highlights the faulty line', async () => {
await next.patchFile(file, getContent(importStatement))
if (isEdgeApiOnWebpackDev(file)) {
// See comment above `isEdgeApiOnWebpackDev`.
await next.fetch(url).catch(() => {})
return
}
const outputIndex = next.cliOutput.length
await retry(async () => {
const res = await next.fetch(url)
expect(res.status).toBe(500)
expectModuleNotFoundDevError(
moduleName,
importStatement,
await res.text(),
next.cliOutput.slice(outputIndex)
)
})
})
}
)
// --- Dynamic import of unused non-existent 3rd party module ---
describe.each(
createVariants({
edgeApi: (imp) => `
export default async function handler(request) {
if (process.env === 'production') {
new (${imp})()
}
return Response.json({ ok: true })
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
export async function middleware(request) {
if (process.env === 'production') {
new (${imp})()
}
return NextResponse.next()
}
`,
})
)(
'$title importing unused 3rd party module',
({ url, file, getContent }) => {
const moduleName = 'not-exist'
const importStatement = `await import("${moduleName}")`
it('throws not-found module error and highlights the faulty line', async () => {
await next.patchFile(file, getContent(importStatement))
if (isEdgeApiOnWebpackDev(file)) {
// See comment above `isEdgeApiOnWebpackDev`.
await next.fetch(url).catch(() => {})
return
}
const outputIndex = next.cliOutput.length
await retry(async () => {
const res = await next.fetch(url)
expect(res.status).toBe(500)
expectModuleNotFoundDevError(
moduleName,
importStatement,
await res.text(),
next.cliOutput.slice(outputIndex)
)
})
})
}
)
// --- Dynamic import of unused node.js module ---
describe.each(
createVariants({
edgeApi: (imp) => `
export default async function handler(request) {
if (process.env === 'production') {
(${imp}).spawn('ls', ['-lh', '/usr'])
}
return Response.json({ ok: true })
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
export async function middleware(request) {
if (process.env === 'production') {
(${imp}).spawn('ls', ['-lh', '/usr'])
}
return NextResponse.next()
}
`,
})
)('$title importing unused node.js module', ({ url, file, getContent }) => {
const moduleName = 'child_process'
const importStatement = `await import("${moduleName}")`
it('does not throw in dev at runtime', async () => {
await next.patchFile(file, getContent(importStatement))
const outputIndex = next.cliOutput.length
await retry(async () => {
const res = await next.fetch(url)
expect(res.status).toBe(200)
expectNoError(moduleName, next.cliOutput.slice(outputIndex))
})
})
})
})
// ==================== PRODUCTION MODE ====================
;(isNextStart ? describe : describe.skip)('production mode', () => {
const { next, isTurbopack, skipped } = nextTestSetup({
files: __dirname,
skipStart: true,
dependencies: {
nanoid: 'latest',
},
skipDeployment: true,
})
if (skipped) return
let originalApi: string
let originalMiddleware: string
let originalLib: string
beforeAll(async () => {
originalApi = await next.readFile('pages/api/route.js')
originalMiddleware = await next.readFile('middleware.js')
originalLib = await next.readFile('lib.js')
})
afterEach(async () => {
try {
await next.stop()
} catch {}
await next.patchFile('pages/api/route.js', originalApi)
await next.patchFile('middleware.js', originalMiddleware)
await next.patchFile('lib.js', originalLib)
})
// --- Dynamic import of node.js module ---
describe.each(
createVariants({
edgeApi: (imp) => `
import { NextResponse } from 'next/server'
export default async function handler(request) {
const { writeFile } = ${imp}
return Response.json({ ok: writeFile() })
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
export async function middleware(request) {
const { writeFile } = ${imp}
return NextResponse.next()
}
`,
})
)(
'$title dynamically importing node.js module',
({ url, file, getContent }) => {
const moduleName = 'fs'
const importStatement = `await import("${moduleName}")`
it('throws unsupported module error at runtime and prints warning in build', async () => {
await next.patchFile(file, getContent(importStatement))
const { cliOutput: buildOutput } = await next.build()
expect(buildOutput).toContain(getUnsupportedModuleWarning(moduleName))
// TODO: should this be failing build or not in turbopack
if (!isTurbopack) {
await next.start()
// `next.start()` resets `cliOutput`, so capture the offset
// after the server is ready to only read runtime output from
// the request below.
const runtimeIndex = next.cliOutput.length
const res = await next.fetch(url)
expect(res.status).toBe(500)
expectUnsupportedModuleProdError(
moduleName,
next.cliOutput.slice(runtimeIndex)
)
}
})
}
)
// --- Dynamic import of node.js module in a lib ---
describe.each(
createVariants({
edgeApi: () => `
import throwAsync from '../../lib'
export default async function handler(request) {
return Response.json({ ok: await throwAsync() })
}
export const config = { runtime: 'edge' }
`,
middleware: () => `
import { NextResponse } from 'next/server'
import throwAsync from './lib'
export async function middleware(request) {
await throwAsync()
return NextResponse.next()
}
`,
lib: (imp) => `
export default async function throwAsync() {
(${imp}).cwd()
}
`,
})
)(
'$title dynamically importing node.js module in a lib',
({ url, file, getContent, getLibContent }) => {
const moduleName = 'os'
const importStatement = `await import("${moduleName}")`
it('throws unsupported module error at runtime and prints warning in build', async () => {
await next.patchFile(file, getContent(importStatement))
if (getLibContent) {
await next.patchFile('lib.js', getLibContent(importStatement))
}
const { cliOutput: buildOutput } = await next.build()
expect(buildOutput).toContain(getUnsupportedModuleWarning(moduleName))
await next.start()
// NOTE: `next.start()` resets `cliOutput`, so capture the offset
// after the server is ready so we only read runtime output from the
// request below.
const runtimeIndex = next.cliOutput.length
const res = await next.fetch(url)
expect(res.status).toBe(500)
expectUnsupportedModuleProdError(
moduleName,
next.cliOutput.slice(runtimeIndex)
)
})
}
)
// --- Static import of non-existent 3rd party module ---
describe.each(
createVariants({
edgeApi: (imp) => `
${imp}
export default async function handler(request) {
new Unknown()
return Response.json({ ok: true })
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
${imp}
export async function middleware(request) {
new Unknown()
return NextResponse.next()
}
`,
})
)(
'$title statically importing 3rd party module',
({ file, getContent }) => {
const moduleName = 'not-exist'
const importStatement = `import Unknown from "${moduleName}"`
it('does not build and reports module not found error', async () => {
await next.patchFile(file, getContent(importStatement))
const { exitCode, cliOutput: buildOutput } = await next.build()
expect(exitCode).toEqual(1)
expectModuleNotFoundProdError(moduleName, buildOutput)
})
}
)
// --- Import vanilla 3rd party module (nanoid) ---
describe.each(
createVariants({
edgeApi: (imp) => `
${imp}
export default async function handler(request) {
const response = Response.json({ ok: true })
response.headers.set('x-from-runtime', nanoid())
return response
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
${imp}
export async function middleware(request) {
const response = NextResponse.next()
response.headers.set('x-from-runtime', nanoid())
return response
}
`,
})
)(
'$title importing vanilla 3rd party module',
({ url, file, getContent }) => {
const moduleName = 'nanoid'
const importStatement = `import { nanoid } from "${moduleName}"`
it('does not throw in production at runtime', async () => {
await next.patchFile(file, getContent(importStatement))
const { cliOutput: buildOutput } = await next.build()
expect(buildOutput).not.toContain(
getUnsupportedModuleWarning(moduleName)
)
await next.start()
const runtimeIndex = next.cliOutput.length
const res = await next.fetch(url)
expect(res.status).toBe(200)
expect(res.headers.get('x-from-runtime')).toBeDefined()
expectNoError(moduleName, next.cliOutput.slice(runtimeIndex))
})
}
)
// --- Buffer polyfill ---
describe.each(
createVariants({
edgeApi: (imp) => `
${imp}
export default async function handler(request) {
const response = Response.json({ ok: true })
response.headers.set('x-from-runtime', Buffer.isBuffer('a string'))
return response
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
${imp}
export async function middleware(request) {
const response = NextResponse.next()
response.headers.set('x-from-runtime', Buffer.isBuffer('a string'))
return response
}
`,
})
)('$title using Buffer polyfill', ({ url, file, getContent }) => {
const moduleName = 'buffer'
const importStatement = `import { Buffer } from "${moduleName}"`
it('does not throw in production at runtime', async () => {
await next.patchFile(file, getContent(importStatement))
await next.build()
await next.start()
const runtimeIndex = next.cliOutput.length
const res = await next.fetch(url)
expect(res.status).toBe(200)
expect(res.headers.get('x-from-runtime')).toBe('false')
expectNoError(moduleName, next.cliOutput.slice(runtimeIndex))
})
})
// --- Static import of node.js module ---
describe.each(
createVariants({
edgeApi: (imp) => `
${imp}
export default async function handler(request) {
basename()
return Response.json({ ok: basename() })
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
${imp}
export async function middleware(request) {
basename()
return NextResponse.next()
}
`,
})
)(
'$title statically importing node.js module',
({ url, file, getContent }) => {
const moduleName = 'fs'
const importStatement = `import { basename } from "${moduleName}"`
it('throws unsupported module error at runtime and prints warning in build', async () => {
await next.patchFile(file, getContent(importStatement))
const { cliOutput: buildOutput } = await next.build()
expect(buildOutput).toContain(getUnsupportedModuleWarning(moduleName))
await next.start()
const runtimeIndex = next.cliOutput.length
const res = await next.fetch(url)
expect(res.status).toBe(500)
expectUnsupportedModuleProdError(
moduleName,
next.cliOutput.slice(runtimeIndex)
)
})
}
)
// --- Dynamic import of non-existent 3rd party module ---
describe.each(
createVariants({
edgeApi: (imp) => `
export default async function handler(request) {
new (${imp})()
return Response.json({ ok: true })
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
export async function middleware(request) {
new (${imp})()
return NextResponse.next()
}
`,
})
)(
'$title dynamically importing 3rd party module',
({ file, getContent }) => {
const moduleName = 'not-exist'
const importStatement = `await import("${moduleName}")`
it('does not build and reports module not found error', async () => {
await next.patchFile(file, getContent(importStatement))
const { exitCode, cliOutput: buildOutput } = await next.build()
expect(exitCode).toEqual(1)
expectModuleNotFoundProdError(moduleName, buildOutput)
})
}
)
// --- Dynamic import of unused non-existent 3rd party module ---
describe.each(
createVariants({
edgeApi: (imp) => `
export default async function handler(request) {
if (process.env === 'production') {
new (${imp})()
}
return Response.json({ ok: true })
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
export async function middleware(request) {
if (process.env === 'production') {
new (${imp})()
}
return NextResponse.next()
}
`,
})
)('$title importing unused 3rd party module', ({ file, getContent }) => {
const moduleName = 'not-exist'
const importStatement = `await import("${moduleName}")`
it('does not build and reports module not found error', async () => {
await next.patchFile(file, getContent(importStatement))
const { exitCode, cliOutput: buildOutput } = await next.build()
expect(exitCode).toEqual(1)
expectModuleNotFoundProdError(moduleName, buildOutput)
})
})
// --- Dynamic import of unused node.js module ---
describe.each(
createVariants({
edgeApi: (imp) => `
export default async function handler(request) {
if (process.env === 'production') {
(${imp}).spawn('ls', ['-lh', '/usr'])
}
return Response.json({ ok: true })
}
export const config = { runtime: 'edge' }
`,
middleware: (imp) => `
import { NextResponse } from 'next/server'
export async function middleware(request) {
if (process.env === 'production') {
(${imp}).spawn('ls', ['-lh', '/usr'])
}
return NextResponse.next()
}
`,
})
)('$title importing unused node.js module', ({ url, file, getContent }) => {
const moduleName = 'child_process'
const importStatement = `await import("${moduleName}")`
it('does not throw in production at runtime', async () => {
await next.patchFile(file, getContent(importStatement))
const { cliOutput: buildOutput } = await next.build()
expect(buildOutput).toContain(getUnsupportedModuleWarning(moduleName))
await next.start()
const runtimeIndex = next.cliOutput.length
const res = await next.fetch(url)
expect(res.status).toBe(200)
const runtimeOutput = next.cliOutput.slice(runtimeIndex)
expect(runtimeOutput).not.toContain(
getUnsupportedModuleWarning(moduleName)
)
expect(runtimeOutput).not.toContain(getModuleNotFound(moduleName))
})
})
})
})