feat(experimental): option to polyfill fetch using undici in Node.js <18 (#40318)

This PR adds a new `experimental.enableUndici` option to let the
developer switch from `next-fetch` to `undici` as the underlying
polyfill for `fetch` in Node.js.

In the current implementation, Next.js makes sure that `fetch` is always
available by using `node-fetch`. However, we do not polyfill in Node.js
18+, since those versions come with their own `fetch` implementation
already, built-in.

Node.js 18+ uses `undici` under the hood, so letting the developer use
`undici` earlier could make the migration easier later on.

Eventually, we hope to be able to stop polyfilling `fetch` in an
upcoming major version of Next.js, shipping less code.


## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the
feature request has been accepted for implementation before opening a
PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [ ] The examples guidelines are followed from [our contributing
doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples)

Co-authored-by: Balázs Orbán <info@balazsorban.com>
Co-authored-by: Sukka <isukkaw@gmail.com>
Co-authored-by: JJ Kasper <jj@jjsweb.site>
Co-authored-by: Steven <steven@ceriously.com>
This commit is contained in:
Ethan Arrowood 2022-09-27 14:37:28 -06:00 committed by GitHub
parent bc685142aa
commit e0cc9cd44f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 493 additions and 157 deletions

View file

@ -3,6 +3,7 @@ const nextJest = require('next/jest')
const createJestConfig = nextJest()
// Any custom config you want to pass to Jest
/** @type {import('jest').Config} */
const customJestConfig = {
testMatch: ['**/*.test.js', '**/*.test.ts', '**/*.test.tsx'],
setupFilesAfterEnv: ['<rootDir>/jest-setup-after-env.ts'],
@ -10,6 +11,9 @@ const customJestConfig = {
rootDir: 'test',
modulePaths: ['<rootDir>/lib'],
transformIgnorePatterns: ['/next[/\\\\]dist/', '/\\.next/'],
globals: {
AbortSignal: global.AbortSignal,
},
}
// createJestConfig is exported in this way to ensure that next/jest can load the Next.js config which is async

View file

@ -1205,6 +1205,7 @@ export default async function build(
configFileName,
runtimeEnvConfig,
httpAgentOptions: config.httpAgentOptions,
enableUndici: config.experimental.enableUndici,
locales: config.i18n?.locales,
defaultLocale: config.i18n?.defaultLocale,
pageRuntime: config.experimental.runtime,
@ -1352,6 +1353,7 @@ export default async function build(
configFileName,
runtimeEnvConfig,
httpAgentOptions: config.httpAgentOptions,
enableUndici: config.experimental.enableUndici,
locales: config.i18n?.locales,
defaultLocale: config.i18n?.defaultLocale,
parentId: isPageStaticSpan.id,

View file

@ -41,7 +41,7 @@ import {
LoadComponentsReturnType,
} from '../server/load-components'
import { trace } from '../trace'
import { setHttpAgentOptions } from '../server/config'
import { setHttpClientAndAgentOptions } from '../server/config'
import { recursiveDelete } from '../lib/recursive-delete'
import { Sema } from 'next/dist/compiled/async-sema'
import { MiddlewareManifest } from './webpack/plugins/middleware-plugin'
@ -1169,6 +1169,7 @@ export async function isPageStatic({
configFileName,
runtimeEnvConfig,
httpAgentOptions,
enableUndici,
locales,
defaultLocale,
parentId,
@ -1184,6 +1185,7 @@ export async function isPageStatic({
configFileName: string
runtimeEnvConfig: any
httpAgentOptions: NextConfigComplete['httpAgentOptions']
enableUndici?: NextConfigComplete['experimental']['enableUndici']
locales?: string[]
defaultLocale?: string
parentId?: any
@ -1210,7 +1212,10 @@ export async function isPageStatic({
return isPageStaticSpan
.traceAsyncFn(async () => {
require('../shared/lib/runtime-config').setConfig(runtimeEnvConfig)
setHttpAgentOptions(httpAgentOptions)
setHttpClientAndAgentOptions({
httpAgentOptions,
experimental: { enableUndici },
})
let componentsResult: LoadComponentsReturnType
let prerenderRoutes: Array<string> | undefined

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) Matteo Collina and Undici contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"name":"undici","main":"index.js","author":"Matteo Collina <hello@matteocollina.com>","license":"MIT"}

View file

@ -619,6 +619,7 @@ export default async function exportApp(
httpAgentOptions: nextConfig.httpAgentOptions,
serverComponents: !!nextConfig.experimental.appDir,
appPaths: options.appPaths || [],
enableUndici: nextConfig.experimental.enableUndici,
})
for (const validation of result.ampValidations || []) {

View file

@ -23,7 +23,7 @@ import { requireFontManifest } from '../server/require'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import { trace } from '../trace'
import { isInAmpMode } from '../shared/lib/amp-mode'
import { setHttpAgentOptions } from '../server/config'
import { setHttpClientAndAgentOptions } from '../server/config'
import RenderResult from '../server/render-result'
import isError from '../lib/is-error'
import { addRequestMeta } from '../server/request-meta'
@ -70,6 +70,7 @@ interface ExportPageInput {
httpAgentOptions: NextConfigComplete['httpAgentOptions']
serverComponents?: boolean
appPaths: string[]
enableUndici: NextConfigComplete['experimental']['enableUndici']
}
interface ExportPageResults {
@ -119,8 +120,12 @@ export default async function exportPage({
disableOptimizedLoading,
httpAgentOptions,
serverComponents,
enableUndici,
}: ExportPageInput): Promise<ExportPageResults> {
setHttpAgentOptions(httpAgentOptions)
setHttpClientAndAgentOptions({
httpAgentOptions,
experimental: { enableUndici },
})
const exportPageSpan = trace('export-page-worker', parentSpanId)
return exportPageSpan.traceAsyncFn(async () => {

View file

@ -264,6 +264,7 @@
"timers-browserify": "2.0.12",
"tty-browserify": "0.0.1",
"ua-parser-js": "0.7.28",
"undici": "5.10.0",
"unistore": "3.4.1",
"util": "0.12.4",
"uuid": "8.3.2",

View file

@ -390,6 +390,9 @@ const configSchema = {
},
type: 'array',
},
enableUndici: {
type: 'boolean',
},
workerThreads: {
type: 'boolean',
},

View file

@ -146,6 +146,7 @@ export interface ExperimentalConfig {
* [webpack/webpack#ModuleNotoundError.js#L13-L42](https://github.com/webpack/webpack/blob/2a0536cf510768111a3a6dceeb14cb79b9f59273/lib/ModuleNotFoundError.js#L13-L42)
*/
fallbackNodePolyfills?: false
enableUndici?: boolean
sri?: {
algorithm?: SubresourceIntegrityAlgorithm
}
@ -585,6 +586,7 @@ export const defaultConfig: NextConfig = {
amp: undefined,
urlImports: undefined,
modularizeImports: undefined,
enableUndici: false,
adjustFontFallbacks: false,
},
}

View file

@ -139,6 +139,7 @@ export function loadWebpackHook() {
'next/dist/compiled/@babel/runtime/package.json',
],
['node-fetch', 'next/dist/compiled/node-fetch'],
['undici', 'next/dist/compiled/undici'],
].map(
// Use dynamic require.resolve to avoid statically analyzable since they're only for build time
([request, replacement]) => [request, require.resolve(replacement)]

View file

@ -13,6 +13,7 @@ import {
ExperimentalConfig,
NextConfigComplete,
validateConfig,
NextConfig,
} from './config-shared'
import { loadWebpackHook } from './config-utils'
import {
@ -22,6 +23,7 @@ import {
} from '../shared/lib/image-config'
import { loadEnvConfig } from '@next/env'
import { hasNextSupport } from '../telemetry/ci-info'
import { gte as semverGte } from 'next/dist/compiled/semver'
export { DomainLocale, NextConfig, normalizeConfig } from './config-shared'
@ -45,12 +47,22 @@ const experimentalWarning = execOnce(
}
)
export function setHttpAgentOptions(
options: NextConfigComplete['httpAgentOptions']
) {
export function setHttpClientAndAgentOptions(options: NextConfig) {
if (semverGte(process.version, '16.8.0')) {
if (semverGte(process.version, '18.0.0')) {
Log.warn(
'`enableUndici` option is unnecessary in Node.js v18.0.0 or greater.'
)
}
;(global as any).__NEXT_USE_UNDICI = options.experimental?.enableUndici
} else if (options.experimental?.enableUndici) {
Log.warn(
'`enableUndici` option requires Node.js v16.8.0 or greater. Falling back to `node-fetch`'
)
}
if ((global as any).__NEXT_HTTP_AGENT) {
// We only need to assign once because we want
// to resuse the same agent for all requests.
// to reuse the same agent for all requests.
return
}
@ -58,8 +70,9 @@ export function setHttpAgentOptions(
throw new Error('Expected config.httpAgentOptions to be an object')
}
;(global as any).__NEXT_HTTP_AGENT = new HttpAgent(options)
;(global as any).__NEXT_HTTPS_AGENT = new HttpsAgent(options)
;(global as any).__NEXT_HTTP_AGENT_OPTIONS = options.httpAgentOptions
;(global as any).__NEXT_HTTP_AGENT = new HttpAgent(options.httpAgentOptions)
;(global as any).__NEXT_HTTPS_AGENT = new HttpsAgent(options.httpAgentOptions)
}
function assignDefaults(userConfig: { [key: string]: any }) {
@ -545,9 +558,7 @@ function assignDefaults(userConfig: { [key: string]: any }) {
// TODO: Change defaultConfig type to NextConfigComplete
// so we don't need "!" here.
setHttpAgentOptions(
result.httpAgentOptions || defaultConfig.httpAgentOptions!
)
setHttpClientAndAgentOptions(result || defaultConfig)
if (result.i18n) {
const { i18n } = result
@ -855,6 +866,6 @@ export default async function loadConfig(
// reactRoot can be updated correctly even with no next.config.js
const completeConfig = assignDefaults(defaultConfig) as NextConfigComplete
completeConfig.configFileName = configFileName
setHttpAgentOptions(completeConfig.httpAgentOptions)
setHttpClientAndAgentOptions(completeConfig)
return completeConfig
}

View file

@ -1286,6 +1286,7 @@ export default class DevServer extends Server {
publicRuntimeConfig,
serverRuntimeConfig,
httpAgentOptions,
experimental: { enableUndici },
} = this.nextConfig
const { locales, defaultLocale } = this.nextConfig.i18n || {}
@ -1299,6 +1300,7 @@ export default class DevServer extends Server {
serverRuntimeConfig,
},
httpAgentOptions,
enableUndici,
locales,
defaultLocale,
originalAppPath,

View file

@ -7,7 +7,7 @@ import {
collectGenerateParams,
} from '../../build/utils'
import { loadComponents } from '../load-components'
import { setHttpAgentOptions } from '../config'
import { setHttpClientAndAgentOptions } from '../config'
type RuntimeConfig = any
@ -22,6 +22,7 @@ export async function loadStaticPaths({
serverless,
config,
httpAgentOptions,
enableUndici,
locales,
defaultLocale,
isAppPath,
@ -32,6 +33,7 @@ export async function loadStaticPaths({
serverless: boolean
config: RuntimeConfig
httpAgentOptions: NextConfigComplete['httpAgentOptions']
enableUndici: NextConfigComplete['enableUndici']
locales?: string[]
defaultLocale?: string
isAppPath?: boolean
@ -49,7 +51,10 @@ export async function loadStaticPaths({
// update work memory runtime-config
require('../../shared/lib/runtime-config').setConfig(config)
setHttpAgentOptions(httpAgentOptions)
setHttpClientAndAgentOptions({
httpAgentOptions,
experimental: { enableUndici },
})
const components = await loadComponents({
distDir,

View file

@ -1,23 +1,56 @@
import fetch, {
Headers,
Request,
Response,
} from 'next/dist/compiled/node-fetch'
// Polyfill fetch() in the Node.js environment
if (!global.fetch) {
const agent = ({ protocol }) =>
protocol === 'http:' ? global.__NEXT_HTTP_AGENT : global.__NEXT_HTTPS_AGENT
const fetchWithAgent = (url, opts, ...rest) => {
if (!opts) {
opts = { agent }
} else if (!opts.agent) {
opts.agent = agent
}
return fetch(url, opts, ...rest)
function getFetchImpl() {
return global.__NEXT_USE_UNDICI
? require('next/dist/compiled/undici')
: require('next/dist/compiled/node-fetch')
}
global.fetch = fetchWithAgent
global.Headers = Headers
global.Request = Request
global.Response = Response
// Due to limitation of global configuration, we have to do this resolution at runtime
global.fetch = (...args) => {
const fetchImpl = getFetchImpl()
if (global.__NEXT_USE_UNDICI) {
// Undici does not support the `keepAlive` option,
// instead we have to pass a custom dispatcher
if (
!global.__NEXT_HTTP_AGENT_OPTIONS?.keepAlive &&
!global.__NEXT_UNDICI_AGENT_SET
) {
global.__NEXT_UNDICI_AGENT_SET = true
fetchImpl.setGlobalDispatcher(new fetchImpl.Agent({ pipelining: 0 }))
}
return fetchImpl.fetch(...args)
}
const agent = ({ protocol }) =>
protocol === 'http:'
? global.__NEXT_HTTP_AGENT
: global.__NEXT_HTTPS_AGENT
if (!args[1]) {
args[1] = { agent }
} else if (!args[1].agent) {
args[1].agent = agent
}
return fetchImpl(...args)
}
Object.defineProperties(global, {
Headers: {
get() {
return getFetchImpl().Headers
},
},
Request: {
get() {
return getFetchImpl().Request
},
},
Response: {
get() {
return getFetchImpl().Response
},
},
})
}

View file

@ -86,6 +86,8 @@ const externals = {
'next/dist/build/webpack/plugins/terser-webpack-plugin',
// TODO: Add @swc/helpers to externals once @vercel/ncc switch to swc-loader
undici: 'undici',
}
// eslint-disable-next-line camelcase
externals['node-html-parser'] = 'next/dist/compiled/node-html-parser'
@ -167,6 +169,14 @@ export async function ncc_node_fetch(task, opts) {
.target('compiled/node-fetch')
}
externals['undici'] = 'next/dist/compiled/undici'
export async function ncc_undici(task, opts) {
await task
.source(opts.src || relative(__dirname, require.resolve('undici')))
.ncc({ packageName: 'undici', externals })
.target('compiled/undici')
}
// eslint-disable-next-line camelcase
export async function compile_config_schema(task, opts) {
const { configSchema } = require('./dist/server/config-schema')
@ -1818,6 +1828,7 @@ export async function ncc(task, opts) {
'ncc_get_orientation',
'ncc_hapi_accept',
'ncc_node_fetch',
'ncc_undici',
'ncc_acorn',
'ncc_amphtml_validator',
'ncc_arg',

View file

@ -28,6 +28,8 @@ declare module 'next/dist/compiled/node-fetch' {
export * from 'node-fetch'
}
declare module 'next/dist/compiled/undici' {}
declare module 'next/dist/compiled/jest-worker' {
export * from 'jest-worker'
}

View file

@ -601,6 +601,7 @@ importers:
timers-browserify: 2.0.12
tty-browserify: 0.0.1
ua-parser-js: 0.7.28
undici: 5.10.0
unistore: 3.4.1
use-sync-external-store: 1.2.0
util: 0.12.4
@ -789,6 +790,7 @@ importers:
timers-browserify: 2.0.12
tty-browserify: 0.0.1
ua-parser-js: 0.7.28
undici: 5.10.0
unistore: 3.4.1
util: 0.12.4
uuid: 8.3.2
@ -3896,7 +3898,7 @@ packages:
integrity: sha512-OoUX2+yhtBH6FGtPoI3gP0YQfjDyLWUzifuvZ3cZwF8AF8Gs7DWM9Lg8/9OfhP4I9ZL8DAuK+hSwxOKdvOLXew==,
}
dependencies:
'@edge-runtime/vm': 1.1.0-beta.33
'@edge-runtime/vm': 1.1.0-beta.34
'@jest/environment': 28.1.3
'@jest/fake-timers': 28.1.3
'@jest/types': 28.1.3
@ -3911,15 +3913,6 @@ packages:
}
dev: true
/@edge-runtime/vm/1.1.0-beta.33:
resolution:
{
integrity: sha512-Aifd/elNDeI01oEzUnCF5URPtMgBIVDhnuy/F6SgS2OMJvzts/U5Rl2hxYliViU2OpC8ZkM/XT/t+Q7rQPJsgw==,
}
dependencies:
'@edge-runtime/primitives': 1.1.0-beta.34
dev: true
/@edge-runtime/vm/1.1.0-beta.34:
resolution:
{
@ -4346,13 +4339,6 @@ packages:
purgecss: 1.4.2
dev: true
/@gar/promisify/1.1.3:
resolution:
{
integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==,
}
dev: true
/@grpc/grpc-js/0.8.1:
resolution:
{
@ -5280,7 +5266,7 @@ packages:
fs-extra: 9.1.0
npm-package-arg: 8.1.0
npmlog: 4.1.2
signal-exit: 3.0.7
signal-exit: 3.0.3
write-pkg: 4.0.0
dev: true
@ -5789,16 +5775,6 @@ packages:
}
dev: true
/@npmcli/fs/1.1.1:
resolution:
{
integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==,
}
dependencies:
'@gar/promisify': 1.1.3
semver: 7.3.7
dev: true
/@npmcli/git/2.0.4:
resolution:
{
@ -8267,7 +8243,7 @@ packages:
dependencies:
acorn: 7.4.1
/acorn-jsx/5.3.1_acorn@8.8.0:
/acorn-jsx/5.3.1_acorn@8.6.0:
resolution:
{
integrity: sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==,
@ -8275,7 +8251,7 @@ packages:
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
dependencies:
acorn: 8.8.0
acorn: 8.6.0
dev: true
/acorn-walk/7.1.1:
@ -9988,14 +9964,13 @@ packages:
engines: { node: '>= 0.8' }
dev: true
/cacache/15.3.0:
/cacache/15.0.5:
resolution:
{
integrity: sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==,
integrity: sha512-lloiL22n7sOjEEXdL8NAjTgv9a1u43xICE9/203qonkZUCj5X1UEWIdf2/Y0d6QcCtMzbKQyhrcDbdvlZTs/+A==,
}
engines: { node: '>= 10' }
dependencies:
'@npmcli/fs': 1.1.1
'@npmcli/move-file': 1.0.1
chownr: 2.0.0
fs-minipass: 2.1.0
@ -11074,7 +11049,10 @@ packages:
dev: true
/concat-map/0.0.1:
resolution: { integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= }
resolution:
{
integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==,
}
/concat-stream/1.6.2:
resolution:
@ -13246,10 +13224,7 @@ packages:
dev: true
/err-code/1.1.2:
resolution:
{
integrity: sha512-CJAN+O0/yA1CKfRn9SXOGctSpEM7DCon/r/5r2eXFMY2zCCJBasFhcM5I+1kh3Ap11FsQCX+vGHceNPvpWKhoA==,
}
resolution: { integrity: sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= }
dev: true
/err-code/2.0.3:
@ -14127,7 +14102,7 @@ packages:
merge-stream: 2.0.0
npm-run-path: 4.0.1
onetime: 5.1.2
signal-exit: 3.0.7
signal-exit: 3.0.3
strip-final-newline: 2.0.0
dev: true
@ -15112,10 +15087,10 @@ packages:
console-control-strings: 1.1.0
has-unicode: 2.0.1
object-assign: 4.1.1
signal-exit: 3.0.7
signal-exit: 3.0.3
string-width: 1.0.2
strip-ansi: 3.0.1
wide-align: 1.1.5
wide-align: 1.1.3
dev: true
/generic-names/2.0.1:
@ -16702,10 +16677,7 @@ packages:
engines: { node: '>=0.8.19' }
/indent-string/2.1.0:
resolution:
{
integrity: sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg==,
}
resolution: { integrity: sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= }
engines: { node: '>=0.10.0' }
dependencies:
repeating: 2.0.1
@ -18747,7 +18719,7 @@ packages:
optional: true
dependencies:
abab: 2.0.5
acorn: 8.8.0
acorn: 8.6.0
acorn-globals: 6.0.0
cssom: 0.4.4
cssstyle: 2.3.0
@ -19195,10 +19167,7 @@ packages:
dev: true
/levn/0.3.0:
resolution:
{
integrity: sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==,
}
resolution: { integrity: sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= }
engines: { node: '>= 0.8.0' }
dependencies:
prelude-ls: 1.1.2
@ -19816,7 +19785,7 @@ packages:
engines: { node: '>=0.10.0' }
dependencies:
currently-unhandled: 0.4.1
signal-exit: 3.0.7
signal-exit: 3.0.3
dev: true
/lower-case-first/1.0.2:
@ -19939,7 +19908,7 @@ packages:
engines: { node: '>= 10' }
dependencies:
agentkeepalive: 4.1.4
cacache: 15.3.0
cacache: 15.0.5
http-cache-semantics: 4.1.0
http-proxy-agent: 4.0.1
https-proxy-agent: 5.0.0
@ -20198,10 +20167,7 @@ packages:
dev: true
/meow/3.7.0:
resolution:
{
integrity: sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==,
}
resolution: { integrity: sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= }
engines: { node: '>=0.10.0' }
dependencies:
camelcase-keys: 2.1.0
@ -20423,8 +20389,8 @@ packages:
integrity: sha512-NQuiYA0lw+eFDtSG4+c7ao3RG9dM4P0Kx/sn8OLyPhxtIc6k+9n14k5VfLxRKfAxYRTo8c5PLZPaRNmslGWxJw==,
}
dependencies:
acorn: 8.8.0
acorn-jsx: 5.3.1_acorn@8.8.0
acorn: 8.6.0
acorn-jsx: 5.3.1_acorn@8.6.0
micromark: 2.11.4
micromark-extension-mdx-expression: 0.3.2
micromark-extension-mdx-jsx: 0.3.3
@ -22222,7 +22188,7 @@ packages:
'@npmcli/installed-package-contents': 1.0.7
'@npmcli/promise-spawn': 1.3.2
'@npmcli/run-script': 1.8.3
cacache: 15.3.0
cacache: 15.0.5
chownr: 2.0.0
fs-minipass: 2.1.0
infer-owner: 1.0.4
@ -23961,10 +23927,7 @@ packages:
dev: true
/prelude-ls/1.1.2:
resolution:
{
integrity: sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==,
}
resolution: { integrity: sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= }
engines: { node: '>= 0.8.0' }
dev: true
@ -24111,10 +24074,7 @@ packages:
dev: true
/process-nextick-args/1.0.7:
resolution:
{
integrity: sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw==,
}
resolution: { integrity: sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= }
dev: true
/process-nextick-args/2.0.1:
@ -24163,10 +24123,7 @@ packages:
dev: true
/promise-retry/1.1.1:
resolution:
{
integrity: sha512-StEy2osPr28o17bIW776GtwO6+Q+M9zPiZkYfosciUUMYqjhU/ffwRAH0zN2+uvGyUsn8/YICIHRzLbPacpZGw==,
}
resolution: { integrity: sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0= }
engines: { node: '>=0.12' }
dependencies:
err-code: 1.1.2
@ -24773,10 +24730,7 @@ packages:
dev: true
/read-pkg-up/1.0.1:
resolution:
{
integrity: sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==,
}
resolution: { integrity: sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= }
engines: { node: '>=0.10.0' }
dependencies:
find-up: 1.1.2
@ -24792,10 +24746,7 @@ packages:
dev: true
/read-pkg-up/3.0.0:
resolution:
{
integrity: sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==,
}
resolution: { integrity: sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= }
engines: { node: '>=4' }
dependencies:
find-up: 2.1.0
@ -24872,10 +24823,7 @@ packages:
dev: true
/readable-stream/1.1.14:
resolution:
{
integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==,
}
resolution: { integrity: sha1-fPTFTvZI44EwhMY23SB54WbAgdk= }
dependencies:
core-util-is: 1.0.2
inherits: 2.0.4
@ -24990,10 +24938,7 @@ packages:
dev: true
/redent/1.0.0:
resolution:
{
integrity: sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g==,
}
resolution: { integrity: sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= }
engines: { node: '>=0.10.0' }
dependencies:
indent-string: 2.1.0
@ -25453,7 +25398,10 @@ packages:
dev: true
/replace-ext/1.0.0:
resolution: { integrity: sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= }
resolution:
{
integrity: sha512-vuNYXC7gG7IeVNBC1xUllqCcZKRbJoSPOBhnTEcAIiKCsbuef6zO3F0Rve3isPMMoNoQRWjQwbAgAjHUHniyEA==,
}
engines: { node: '>= 0.10' }
dev: true
@ -25501,10 +25449,7 @@ packages:
dev: true
/require-directory/2.1.1:
resolution:
{
integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==,
}
resolution: { integrity: sha1-jGStX9MNqxyXbiNE/+f3kqam30I= }
engines: { node: '>=0.10.0' }
dev: true
@ -25647,7 +25592,7 @@ packages:
engines: { node: '>=4' }
dependencies:
onetime: 2.0.1
signal-exit: 3.0.7
signal-exit: 3.0.3
dev: true
/restore-cursor/3.1.0:
@ -25658,7 +25603,7 @@ packages:
engines: { node: '>=8' }
dependencies:
onetime: 5.1.2
signal-exit: 3.0.7
signal-exit: 3.0.3
/restore-cursor/4.0.0:
resolution:
@ -25668,7 +25613,7 @@ packages:
engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 }
dependencies:
onetime: 5.1.2
signal-exit: 3.0.7
signal-exit: 3.0.3
dev: true
/ret/0.1.15:
@ -26422,6 +26367,7 @@ packages:
{
integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==,
}
dev: true
/simple-swizzle/0.2.2:
resolution:
@ -26474,10 +26420,7 @@ packages:
dev: true
/slice-ansi/0.0.4:
resolution:
{
integrity: sha512-up04hB2hR92PgjpyU3y/eg91yIBILyjVY26NvvciY3EVVPjybkMszMpXQ9QAkcS3I5rtJBDLoTxxg+qvW8c7rw==,
}
resolution: { integrity: sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= }
engines: { node: '>=0.10.0' }
dev: true
@ -28121,10 +28064,7 @@ packages:
dev: true
/trim-newlines/1.0.0:
resolution:
{
integrity: sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==,
}
resolution: { integrity: sha1-WIeWa7WCpFA6QetST301ARgVphM= }
engines: { node: '>=0.10.0' }
dev: true
@ -28331,10 +28271,7 @@ packages:
dev: true
/type-check/0.3.2:
resolution:
{
integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==,
}
resolution: { integrity: sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= }
engines: { node: '>= 0.8.0' }
dependencies:
prelude-ls: 1.1.2
@ -28535,6 +28472,14 @@ packages:
engines: { node: '>=0.10.0' }
dev: true
/undici/5.10.0:
resolution:
{
integrity: sha512-c8HsD3IbwmjjbLvoZuRI26TZic+TSEe8FPMLLOkN1AfYRhdjnKBU6yL+IwcSCbdZiX4e5t0lfMDLDCqj4Sq70g==,
}
engines: { node: '>=12.18' }
dev: true
/unfetch/4.1.0:
resolution:
{
@ -29260,7 +29205,10 @@ packages:
dev: true
/verror/1.10.0:
resolution: { integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= }
resolution:
{
integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==,
}
engines: { '0': node >=0.6.0 }
dependencies:
assert-plus: 1.0.0
@ -29761,13 +29709,13 @@ packages:
dependencies:
isexe: 2.0.0
/wide-align/1.1.5:
/wide-align/1.1.3:
resolution:
{
integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==,
integrity: sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==,
}
dependencies:
string-width: 4.2.3
string-width: 2.1.1
dev: true
/widest-line/3.1.0:
@ -29792,10 +29740,7 @@ packages:
dev: true
/wrap-ansi/3.0.1:
resolution:
{
integrity: sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ==,
}
resolution: { integrity: sha1-KIoE2H7aXChuBg3+jxNc6NAH+Lo= }
engines: { node: '>=4' }
dependencies:
string-width: 2.1.1
@ -29861,7 +29806,7 @@ packages:
dependencies:
imurmurhash: 0.1.4
is-typedarray: 1.0.0
signal-exit: 3.0.3
signal-exit: 3.0.7
typedarray-to-buffer: 3.1.5
dev: true

View file

@ -0,0 +1,129 @@
import { createNext } from 'e2e-utils'
import { NextInstance } from 'test/lib/next-modes/base'
import { fetchViaHTTP } from 'next-test-utils'
import semver from 'semver'
if (
semver.lt(process.version, '16.8.0') ||
semver.gte(process.version, '18.0.0')
) {
it('skipping for Node.js versions <16.8.0 and >18.0.0', () => {
expect(true).toBe(true)
})
} else {
describe('undici fetch', () => {
let next: NextInstance
beforeAll(async () => {
next = await createNext({
files: {
'pages/api/globalFetch.js': `
import { ReadableStream } from 'node:stream/web';
export default async function globalFetch(req, res) {
try {
const response = await fetch('https://example.vercel.sh')
res.json({ value: response.body instanceof ReadableStream })
} catch (error) {
console.error(error);
res.send(error);
}
}
`,
'pages/api/globalHeaders.js': `
export default async function globalHeaders(req, res) {
res.json({
value: (new Headers())[Symbol.iterator].name === 'entries'
})
}
`,
'pages/api/globalRequest.js': `
export default async function globalRequest(req, res) {
res.json({
value: (new Request('https://example.vercel.sh')).headers[Symbol.iterator].name === 'entries'
})
}
`,
'pages/api/globalResponse.js': `
export default async function globalResponse(req, res) {
res.json({
value: (new Response()).headers[Symbol.iterator].name === 'entries'
})
}
`,
},
dependencies: {},
nextConfig: {
experimental: {
enableUndici: true,
},
},
})
})
afterAll(() => next.destroy())
describe('undici', () => {
it('global fetch should return true when undici is used', async () => {
const result = await fetchViaHTTP(next.url, '/api/globalFetch')
const data = await result.json()
expect(data.value).toBe(true)
})
it('global Headers should return true when undici is used', async () => {
const result = await fetchViaHTTP(next.url, '/api/globalHeaders')
const data = await result.json()
expect(data.value).toBe(true)
})
it('global Request should return true when undici is used', async () => {
const result = await fetchViaHTTP(next.url, '/api/globalRequest')
const data = await result.json()
expect(data.value).toBe(true)
})
it('global Response should return true when undici is used', async () => {
const result = await fetchViaHTTP(next.url, '/api/globalResponse')
const data = await result.json()
expect(data.value).toBe(true)
})
})
describe('node-fetch', () => {
beforeAll(async () => {
await next.stop()
await next.patchFile(
'next.config.js',
`module.exports = ${JSON.stringify({
experimental: {
enableUndici: false,
},
})}`
)
await next.start()
})
it('global fetch should return false when node-fetch is used', async () => {
const result = await fetchViaHTTP(next.url, '/api/globalFetch')
const data = await result.json()
expect(data.value).toBe(false)
})
it('global Headers should return false when node-fetch is used', async () => {
const result = await fetchViaHTTP(next.url, '/api/globalHeaders')
const data = await result.json()
expect(data.value).toBe(false)
})
it('global Request should return false when node-fetch is used', async () => {
const result = await fetchViaHTTP(next.url, '/api/globalRequest')
const data = await result.json()
expect(data.value).toBe(false)
})
it('global Response should return false when node-fetch is used', async () => {
const result = await fetchViaHTTP(next.url, '/api/globalResponse')
const data = await result.json()
expect(data.value).toBe(false)
})
})
})
}

View file

@ -0,0 +1,5 @@
module.exports = {
experimental: {
enableUndici: true,
},
}

View file

@ -0,0 +1,5 @@
export default async function handler(_req, res) {
const fetchRes = await fetch('http://localhost:44001')
const props = await fetchRes.json()
res.json(props)
}

View file

@ -0,0 +1,23 @@
export default function Blog(props) {
return <pre id="props">{JSON.stringify(props)}</pre>
}
export async function getStaticProps({ params: { slug } }) {
return { props: { slug } }
}
export async function getStaticPaths() {
const res = await fetch('http://localhost:44001')
const obj = await res.json()
if (obj.connection === 'keep-alive') {
return {
paths: [{ params: { slug: 'first' } }],
fallback: false,
}
}
return {
paths: [],
fallback: false,
}
}

View file

@ -0,0 +1,9 @@
export default function SSG(props) {
return <pre id="props">{JSON.stringify(props)}</pre>
}
export async function getStaticProps() {
const res = await fetch('http://localhost:44001')
const props = await res.json()
return { props }
}

View file

@ -0,0 +1,9 @@
export default function SSR(props) {
return <pre id="props">{JSON.stringify(props)}</pre>
}
export async function getServerSideProps() {
const res = await fetch('http://localhost:44001')
const props = await res.json()
return { props }
}

View file

@ -0,0 +1,99 @@
/* eslint-env jest */
import { join } from 'path'
import { createServer } from 'http'
import {
fetchViaHTTP,
nextBuild,
findPort,
nextStart,
launchApp,
killApp,
} from 'next-test-utils'
import webdriver from 'next-webdriver'
const appDir = join(__dirname, '../')
let appPort
let app
let mockServer
describe('undici-keep-alive', () => {
describe('dev', () => {
beforeAll(async () => {
mockServer = createServer((req, res) => {
// we can test request headers by sending them
// back with the response
const { connection } = req.headers
res.end(JSON.stringify({ connection }))
})
mockServer.listen(44001)
appPort = await findPort()
app = await launchApp(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
mockServer.close()
})
runTests()
})
describe('production', () => {
beforeAll(async () => {
mockServer = createServer((req, res) => {
// we can test request headers by sending them
// back with the response
const { connection } = req.headers
res.end(JSON.stringify({ connection }))
})
mockServer.listen(44001)
const { stdout, stderr } = await nextBuild(appDir, [], {
stdout: true,
stderr: true,
})
if (stdout) console.log(stdout)
if (stderr) console.error(stderr)
appPort = await findPort()
app = await nextStart(appDir, appPort)
})
afterAll(async () => {
await killApp(app)
mockServer.close()
})
runTests()
})
function runTests() {
it('should send keep-alive for json API', async () => {
const res = await fetchViaHTTP(appPort, '/api/json')
const obj = await res.json()
expect(obj).toEqual({ connection: 'keep-alive' })
})
it('should send keep-alive for getStaticProps', async () => {
const browser = await webdriver(appPort, '/ssg')
const props = await browser.elementById('props').text()
const obj = JSON.parse(props)
expect(obj).toEqual({ connection: 'keep-alive' })
await browser.close()
})
it('should send keep-alive for getStaticPaths', async () => {
const browser = await webdriver(appPort, '/blog/first')
const props = await browser.elementById('props').text()
const obj = JSON.parse(props)
expect(obj).toEqual({ slug: 'first' })
await browser.close()
})
it('should send keep-alive for getServerSideProps', async () => {
const browser = await webdriver(appPort, '/ssr')
const props = await browser.elementById('props').text()
const obj = JSON.parse(props)
expect(obj).toEqual({ connection: 'keep-alive' })
await browser.close()
})
}
})