mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
8141dcf12e
### 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.
985 lines
33 KiB
TypeScript
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))
|
|
})
|
|
})
|
|
})
|
|
})
|