diff --git a/docs/API.md b/docs/API.md index bbf24ceba1..f83f30d767 100644 --- a/docs/API.md +++ b/docs/API.md @@ -66,10 +66,16 @@ Bundles `entry` for the given `platform`, and saves it to location `out`. If `so ### `async runServer(config, )` -**Basic options:** `host`, `port`, `secureServerOptions`, `secure (DEPRECATED)`, `secureKey (DEPRECATED)`, `secureCert (DEPRECATED)` +**Basic options:** `host`, `port`, `secureServerOptions`, `unstable_middleware`, `unstable_priorityMiddleware`, `unstable_onServerCreated`, `secure (DEPRECATED)`, `secureKey (DEPRECATED)`, `secureCert (DEPRECATED)` Starts a full Metro HTTP server. It will listen on the specified `host:port`, and can then be queried to retrieve bundles for various entry points. If the `secureServerOptions` family of options are present, the server will be exposed over HTTPS. +`unstable_middleware` and `unstable_priorityMiddleware` take the same shapes as [`server.unstable_middleware`](./Configuration.md#unstable_middleware-experimental) and [`server.unstable_priorityMiddleware`](./Configuration.md#unstable_prioritymiddleware-experimental), and wrap them: `unstable_middleware` passed here runs after the config's, and `unstable_priorityMiddleware` runs before the config's. + +Priority middleware handlers must call `next()` for every request they don't fully handle, so that Metro can serve bundles, source maps, assets and its other endpoints. + +`unstable_onServerCreated` is called with Metro's server instance before the HTTP server starts listening, after [`server.unstable_onServerCreated`](./Configuration.md#unstable_onservercreated-experimental). + `secure`, `secureKey`, `secureCert` are now deprecated and will be removed in a later release. The presence of `secureServerOptions`, along with its options will make Metro run over https. ### `createConnectMiddleware(config, )` diff --git a/docs/Configuration.md b/docs/Configuration.md index 8f057477f8..a0797d3cd6 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -687,7 +687,9 @@ Whether we should enable CMD+R hotkey for refreshing the bundle. Type: `(Middleware, MetroServer) => Middleware` -A function that allows attaching custom [`connect`](https://www.npmjs.com/package/connect) middleware to Metro. For example: +A function that allows attaching custom [`connect`](https://www.npmjs.com/package/connect) middleware to Metro. Metro logs a deprecation warning when this option is set to anything other than its default. Use [`unstable_middleware`](#unstable_middleware-experimental) or [`unstable_priorityMiddleware`](#unstable_prioritymiddleware-experimental) instead, and [`unstable_onServerCreated`](#unstable_onservercreated-experimental) to access the Metro server instance. + +For example: :::tip You can use [`connect()`](https://www.npmjs.com/package/connect#mount-middleware) as a utility to extend the base `metroMiddleware` and to mount additional middleware handlers. @@ -703,6 +705,42 @@ enhanceMiddleware: (metroMiddleware: Middleware, metroServer: MetroServer) => { The `Middleware` type is an alias for [`connect.HandleFunction`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/876b9ec96ba02d0c84b1e49af5890c8f5aa2dfe3/types/connect/index.d.ts#L29). +#### `unstable_middleware`
Experimental
+ +Type: `Array` + +Custom [`connect`](https://www.npmjs.com/package/connect) middleware list allowing integrators to extend Metro's dev server endpoints, mounted in array order. A `[path, handler]` entry mounts the handler with `connect` semantics: it matches the path as a prefix and strips it from `req.url`, keeping the full URL in `req.originalUrl`. + +[`runServer`](./API.md#async-runserverconfig-options) also takes an `unstable_middleware` option, which runs after these handlers. + +```js +server: { + unstable_middleware: [ + ['/custom-endpoint', customEndpointMiddleware()], + notFoundPage, + ], + unstable_priorityMiddleware: [corsMiddleware], +}, +``` + +#### `unstable_priorityMiddleware`
Experimental
+ +Type: `Array` + +Like [`unstable_middleware`](#unstable_middleware-experimental), but runs before Metro. These handlers see the URL before [`rewriteRequestUrl`](#rewriterequesturl) is applied. + +Each priority middleware handler must call `next()` for every request it doesn't fully handle itself. Otherwise the request never reaches Metro, which builds and serves bundles, source maps and assets, symbolicates stack traces, and serves its other dev server endpoints. + +[`runServer`](./API.md#async-runserverconfig-options) also takes an `unstable_priorityMiddleware` option, which runs before these handlers. + +#### `unstable_onServerCreated`
Experimental
+ +Type: `(MetroServer) => void` + +Called with Metro's server instance when [`runServer`](./API.md#async-runserverconfig-options) or [`createConnectMiddleware`](./API.md#createconnectmiddlewareconfig-options) creates it, after the bundler is ready when `waitForBundler` is set, and before `runServer` starts the HTTP server. + +`runServer` also takes an `unstable_onServerCreated` option, which is called after this one. + #### `rewriteRequestUrl` Type: `string => string` @@ -830,6 +868,8 @@ This allows overriding and removing default config parameters such as `platforms ::: +`server.unstable_middleware` and `server.unstable_priorityMiddleware` are combined instead: later configs wrap earlier ones, so their `unstable_middleware` runs last and their `unstable_priorityMiddleware` runs first. An entry already in an earlier config, e.g. copied with a spread, is kept once. + #### Merging Example ```typescript diff --git a/packages/metro-config/API.md b/packages/metro-config/API.md index 58a8d84074..3df6adb4e7 100644 --- a/packages/metro-config/API.md +++ b/packages/metro-config/API.md @@ -187,6 +187,9 @@ export type ServerConfigT = { forwardClientLogs: boolean; port: number; rewriteRequestUrl: (url: string) => string; + unstable_middleware: ReadonlyArray; + unstable_onServerCreated: (metroServer: Server) => void; + unstable_priorityMiddleware: ReadonlyArray; unstable_serverRoot: null | undefined | string; useGlobalHotkey: boolean; verifyConnections: boolean; @@ -200,6 +203,8 @@ export type ServerConfigT = { }; }; +export type ServerMiddleware = Middleware | Readonly<[path: string, handler: Middleware]>; + export type SymbolicatorConfigT = { customizeFrame: (frame: { readonly file: null | undefined | string; diff --git a/packages/metro-config/src/__tests__/mergeConfig-test.js b/packages/metro-config/src/__tests__/mergeConfig-test.js index 502c678417..89a7121ed5 100644 --- a/packages/metro-config/src/__tests__/mergeConfig-test.js +++ b/packages/metro-config/src/__tests__/mergeConfig-test.js @@ -9,7 +9,7 @@ * @oncall react_native */ -import type {InputConfigT} from '../types'; +import type {InputConfigT, ServerMiddleware} from '../types'; import type {CustomResolver} from 'metro-resolver'; import {mergeConfig} from '../loadConfig'; @@ -205,6 +205,36 @@ describe('mergeConfig', () => { }); }); + describe('server middleware merging', () => { + const a: ServerMiddleware = (req, res, next) => next(); + const b: ServerMiddleware = (req, res, next) => next(); + const c: ServerMiddleware = ['/c', (req, res, next) => next()]; + + test('later configs wrap earlier ones', () => { + const result = mergeConfig( + {server: {unstable_priorityMiddleware: [a], unstable_middleware: [a]}}, + {server: {unstable_priorityMiddleware: [b], unstable_middleware: [b]}}, + ); + expect(result.server?.unstable_priorityMiddleware).toEqual([b, a]); + expect(result.server?.unstable_middleware).toEqual([a, b]); + }); + + test('keeps entries copied from the base config once', () => { + const result = mergeConfig( + {server: {unstable_priorityMiddleware: [a, c]}}, + (config: InputConfigT) => ({ + server: { + unstable_priorityMiddleware: [ + b, + ...(config.server?.unstable_priorityMiddleware ?? []), + ], + }, + }), + ); + expect(result.server?.unstable_priorityMiddleware).toEqual([b, a, c]); + }); + }); + describe('resolver path resolution', () => { // `resolve()` maps a module specifier to an absolute path, relative to // metro-config. Without it, these paths are later `require`d from diff --git a/packages/metro-config/src/defaults/defaults.js b/packages/metro-config/src/defaults/defaults.js index f0e7588bbf..96d04763ed 100644 --- a/packages/metro-config/src/defaults/defaults.js +++ b/packages/metro-config/src/defaults/defaults.js @@ -9,7 +9,7 @@ * @oncall react_native */ -import type {PerfLogger, RootPerfLogger} from '../types'; +import type {ConfigT, PerfLogger, RootPerfLogger} from '../types'; export {default as defaultCreateModuleIdFactory} from './createModuleIdFactory'; @@ -63,6 +63,10 @@ export const platforms: Array = ['ios', 'android', 'windows', 'web']; export const DEFAULT_METRO_MINIFIER_PATH = 'metro-minify-terser'; +// Metro warns about `server.enhanceMiddleware` unless it is this function. +export const defaultEnhanceMiddleware: ConfigT['server']['enhanceMiddleware'] = + (middleware, _server) => middleware; + export const noopPerfLoggerFactory = (): RootPerfLogger => { class Logger { start() {} diff --git a/packages/metro-config/src/defaults/index.js b/packages/metro-config/src/defaults/index.js index 3da8b3b439..8c4a51a945 100644 --- a/packages/metro-config/src/defaults/index.js +++ b/packages/metro-config/src/defaults/index.js @@ -17,6 +17,7 @@ import { assetExts, assetResolutions, defaultCreateModuleIdFactory, + defaultEnhanceMiddleware, noopPerfLoggerFactory, platforms, sourceExts, @@ -75,10 +76,16 @@ const getDefaultValues = (projectRoot: ?string): ConfigT => ({ }, server: { - enhanceMiddleware: (middleware, _) => middleware, + enhanceMiddleware: defaultEnhanceMiddleware, forwardClientLogs: true, port: 8081, rewriteRequestUrl: url => url, + // Keep empty to not shadows a Metro builtin or user entry paths + // merged with user middleware as [...defaults, ...user] + unstable_middleware: [], + unstable_onServerCreated: () => {}, + // merged with user middleware as [...user, ...defaults] + unstable_priorityMiddleware: [], unstable_serverRoot: null, useGlobalHotkey: true, verifyConnections: false, diff --git a/packages/metro-config/src/loadConfig.js b/packages/metro-config/src/loadConfig.js index 551b38216a..59c4b0e298 100644 --- a/packages/metro-config/src/loadConfig.js +++ b/packages/metro-config/src/loadConfig.js @@ -9,7 +9,12 @@ * @oncall react_native */ -import type {ConfigT, InputConfigT, YargArguments} from './types'; +import type { + ConfigT, + InputConfigT, + ServerMiddleware, + YargArguments, +} from './types'; import getDefaultConfig from './defaults'; import validConfig from './defaults/validConfig'; @@ -152,6 +157,7 @@ function mergeConfigObjects( server: { ...base.server, ...overrides.server, + ...mergeServerHandlers(base.server, overrides.server), // $FlowFixMe[exponential-spread] ...(base.server?.tls != null ? {tls: base.server?.tls} : null), // only override base tls config with false or an object @@ -195,6 +201,48 @@ function mergeConfigObjects( }; } +// Later configs wrap earlier ones: their `unstable_middleware` runs last and +// their `unstable_priorityMiddleware` runs first. +function mergeServerHandlers( + base: InputConfigT['server'], + overrides: InputConfigT['server'], +): { + unstable_middleware?: ReadonlyArray, + unstable_priorityMiddleware?: ReadonlyArray, +} { + const merged: { + unstable_middleware?: ReadonlyArray, + unstable_priorityMiddleware?: ReadonlyArray, + } = {}; + const middleware = concatUnique( + base?.unstable_middleware, + overrides?.unstable_middleware, + ); + if (middleware != null) { + merged.unstable_middleware = middleware; + } + const priorityMiddleware = concatUnique( + overrides?.unstable_priorityMiddleware, + base?.unstable_priorityMiddleware, + ); + if (priorityMiddleware != null) { + merged.unstable_priorityMiddleware = priorityMiddleware; + } + return merged; +} + +// An entry in both lists (e.g. copied by spreading the base config) is kept +// once, at its first position. +function concatUnique( + first: ?ReadonlyArray, + second: ?ReadonlyArray, +): ?ReadonlyArray { + if (first == null || second == null) { + return first ?? second; + } + return [...new Set([...first, ...second])]; +} + async function mergeConfigAsync( baseConfig: Promise, ...reversedConfigs: ReadonlyArray< diff --git a/packages/metro-config/src/types.js b/packages/metro-config/src/types.js index 2d359280a3..dbc936bdf2 100644 --- a/packages/metro-config/src/types.js +++ b/packages/metro-config/src/types.js @@ -60,6 +60,11 @@ export type GetTransformOptions = ( export type Middleware = HandleFunction; +export type ServerMiddleware = + | Middleware + // Mounted with connect semantics: prefix match, stripped from `req.url`. + | Readonly<[path: string, handler: Middleware]>; + type PerfAnnotations = Partial<{ string: Readonly<{[key: string]: string}>, int: Readonly<{[key: string]: number}>, @@ -184,7 +189,7 @@ type MetalConfigT = { type CacheStoresConfigT = ReadonlyArray>>; type ServerConfigT = { - /** @deprecated */ + /** @deprecated Use `unstable_middleware`, `unstable_priorityMiddleware`, or `unstable_onServerCreated` */ enhanceMiddleware: ( middleware: Middleware, server: MetroServer, @@ -192,6 +197,17 @@ type ServerConfigT = { forwardClientLogs: boolean, port: number, rewriteRequestUrl: (url: string) => string, + // Only receives requests that Metro endpoints did not handle. + // `runServer`'s `unstable_middleware` option runs after these handlers. + unstable_middleware: ReadonlyArray, + // Called with Metro's server instance before the HTTP server listens. + // `runServer`'s `unstable_onServerCreated` option is called after it. + unstable_onServerCreated: (metroServer: MetroServer) => void, + // Runs before Metro endpoints, which only receive a request if these + // handlers call `next()`. + // `runServer`'s `unstable_priorityMiddleware` option runs before these + // handlers. + unstable_priorityMiddleware: ReadonlyArray, unstable_serverRoot: ?string, useGlobalHotkey: boolean, verifyConnections: boolean, diff --git a/packages/metro/API.md b/packages/metro/API.md index e955a43129..b4afc4df23 100644 --- a/packages/metro/API.md +++ b/packages/metro/API.md @@ -280,6 +280,9 @@ export type RunServerOptions = Readonly<{ secureCert?: string | undefined; secureKey?: string | undefined; unstable_extraMiddleware?: ReadonlyArray | undefined; + unstable_middleware?: ReadonlyArray | undefined; + unstable_onServerCreated?: ((metroServer: MetroServer) => void) | undefined; + unstable_priorityMiddleware?: ReadonlyArray | undefined; waitForBundler?: boolean | undefined; watch?: boolean | undefined; websocketEndpoints?: Readonly<{[path: string]: Server_4}> | undefined; diff --git a/packages/metro/src/index.flow.js b/packages/metro/src/index.flow.js index 4b37a9dec5..a580ef8dc0 100644 --- a/packages/metro/src/index.flow.js +++ b/packages/metro/src/index.flow.js @@ -13,13 +13,14 @@ import type {AssetData} from './Assets'; import type {ReadOnlyGraph} from './DeltaBundler'; import type {ServerOptions} from './Server'; import type {BuildOptions, OutputOptions, RequestOptions} from './shared/types'; -import type {HandleFunction} from 'connect'; +import type {HandleFunction, Server as ConnectServer} from 'connect'; import type {TransformProfile} from 'metro-babel-transformer'; import type { ConfigT, InputConfigT, MetroConfig, Middleware, + ServerMiddleware, } from 'metro-config'; import type {CustomResolverOptions} from 'metro-resolver'; import type {CustomTransformOptions} from 'metro-transform-worker'; @@ -48,6 +49,7 @@ import { mergeConfig, resolveConfig, } from 'metro-config'; +import {defaultEnhanceMiddleware} from 'metro-config/private/defaults/defaults'; import {Terminal} from 'metro-core'; import fs from 'node:fs'; import http from 'node:http'; @@ -80,7 +82,13 @@ export type RunServerOptions = Readonly<{ secure?: boolean, // deprecated secureCert?: string, // deprecated secureKey?: string, // deprecated - unstable_extraMiddleware?: ReadonlyArray, + unstable_extraMiddleware?: ReadonlyArray, // deprecated + // Runs after `config.server.unstable_middleware`. + unstable_middleware?: ReadonlyArray, + // Called after `config.server.unstable_onServerCreated`. + unstable_onServerCreated?: (metroServer: MetroServer) => void, + // Runs before `config.server.unstable_priorityMiddleware`. + unstable_priorityMiddleware?: ReadonlyArray, waitForBundler?: boolean, watch?: boolean, websocketEndpoints?: Readonly<{ @@ -235,11 +243,20 @@ export const createConnectMiddleware = async function ( options?: RunMetroOptions, ): Promise { const metroServer = await runMetro(config, options); + config.server.unstable_onServerCreated(metroServer); let enhancedMiddleware: Middleware = metroServer.processRequest; // Enhance the resulting middleware using the config options if (config.server.enhanceMiddleware) { + if (config.server.enhanceMiddleware !== defaultEnhanceMiddleware) { + logDeprecationWarning( + 'The `server.enhanceMiddleware` config option is deprecated and will be ' + + 'removed in a later release. Please use `server.unstable_middleware` ' + + 'or `server.unstable_priorityMiddleware` instead, and ' + + '`server.unstable_onServerCreated` to access the Metro server instance.', + ); + } enhancedMiddleware = config.server.enhanceMiddleware( enhancedMiddleware, metroServer, @@ -267,7 +284,11 @@ export const createConnectMiddleware = async function ( }); }, metroServer, - middleware: enhancedMiddleware, + middleware: composeMiddleware( + config.server.unstable_priorityMiddleware, + enhancedMiddleware, + config.server.unstable_middleware, + ), async end(): Promise { await metroServer.end(); }, @@ -288,7 +309,10 @@ export const runServer = async ( secure, //deprecated secureCert, // deprecated secureKey, // deprecated - unstable_extraMiddleware, + unstable_extraMiddleware = [], // deprecated + unstable_middleware = [], + unstable_onServerCreated, + unstable_priorityMiddleware = [], waitForBundler = false, websocketEndpoints: userWebsocketEndpoints = {}, watch, @@ -296,20 +320,12 @@ export const runServer = async ( await earlyPortCheck(host, config.server.port); if (secure != null || secureCert != null || secureKey != null) { - // eslint-disable-next-line no-console - console.warn( - util.styleText(['inverse', 'yellow', 'bold'], ' DEPRECATED '), + logDeprecationWarning( 'The `secure`, `secureCert`, and `secureKey` options are now deprecated. ' + 'Please use the `secureServerOptions` object instead to pass options to ' + "Metro's https development server, or `config.server.tls` in Metro's configuration", ); } - // Lazy require - // eslint-disable-next-line import/no-commonjs - const connect = require('connect'); - - const serverApp = connect(); - const { middleware, end: endMiddleware, @@ -320,11 +336,13 @@ export const runServer = async ( watch, }); - for (const handler of unstable_extraMiddleware ?? []) { - serverApp.use(handler); - } + unstable_onServerCreated?.(metroServer); - serverApp.use(middleware); + const serverApp = composeMiddleware( + [...unstable_extraMiddleware, ...unstable_priorityMiddleware], + middleware, + unstable_middleware, + ); let httpServer; @@ -590,3 +608,30 @@ async function earlyPortCheck(host: void | string, port: number) { await new Promise(resolve => server.close(() => resolve())); } } + +function logDeprecationWarning(message: string): void { + // eslint-disable-next-line no-console + console.warn( + util.styleText(['inverse', 'yellow', 'bold'], ' DEPRECATED '), + message, + ); +} + +function composeMiddleware( + before: ReadonlyArray, + inner: Middleware, + after: ReadonlyArray, +): ConnectServer { + // Lazy require + // eslint-disable-next-line import/no-commonjs + const connect = require('connect'); + const app = connect(); + for (const entry of [...before, inner, ...after]) { + if (typeof entry === 'function') { + app.use(entry); + } else { + app.use(entry[0], entry[1]); + } + } + return app; +} diff --git a/packages/metro/src/integration_tests/__tests__/server-extraMiddleware-test.js b/packages/metro/src/integration_tests/__tests__/server-extraMiddleware-test.js new file mode 100644 index 0000000000..b8ff935dd8 --- /dev/null +++ b/packages/metro/src/integration_tests/__tests__/server-extraMiddleware-test.js @@ -0,0 +1,179 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @oncall react_native + */ + +'use strict'; + +const Metro = require('../../..'); +const MetroServer = require('../../Server').default; + +jest.useRealTimers(); +jest.setTimeout(60 * 1000); + +// Workaround for https://github.com/nodejs/node/issues/54484: +// Fetch with {Connection: 'close'} to prevent Node reusing connections across tests +const fetchAndClose = (path: string) => + fetch(path, { + headers: {Connection: 'close'}, + }); + +const BUNDLE_PATH = '/TestBundle.bundle?platform=ios&dev=true&minify=false'; + +describe('runServer custom middleware', () => { + let httpServer; + let serverClosedPromise; + + const loadConfig = (server: {...} = {}) => + Metro.loadConfig({config: require.resolve('../metro.config.js')}, {server}); + + async function startServer(config: {...}, options: {...} = {}) { + let onCloseResolve; + serverClosedPromise = new Promise(resolve => (onCloseResolve = resolve)); + ({httpServer} = await Metro.runServer(config, { + reporter: {update() {}}, + onClose: () => { + onCloseResolve(); + }, + ...options, + })); + } + + // Reads the body: undici crashes if the server closes the socket while an + // unread body is paused. + const request = async (pathname: string) => { + const response = await fetchAndClose( + 'http://localhost:' + httpServer.address().port + pathname, + ); + return {status: response.status, text: await response.text()}; + }; + + afterEach(async () => { + if (httpServer != null) { + httpServer.close(); + await serverClosedPromise; + httpServer = null; + } + }); + + test('mounts config middleware inside framework middleware', async () => { + const calls = []; + const record = + (name: string) => + (req, res, next): void => { + calls.push(name); + next(); + }; + const config = await loadConfig({ + unstable_priorityMiddleware: [record('config priority')], + unstable_middleware: [record('config middleware')], + }); + await startServer(config, { + unstable_priorityMiddleware: [record('framework priority')], + unstable_middleware: [ + record('framework middleware'), + (req, res) => { + res.end('not found'); + }, + ], + }); + + expect((await request(BUNDLE_PATH)).status).toBe(200); + expect(calls).toEqual(['framework priority', 'config priority']); + + calls.length = 0; + expect((await request('/unhandled')).text).toBe('not found'); + expect(calls).toEqual([ + 'framework priority', + 'config priority', + 'config middleware', + 'framework middleware', + ]); + }); + + test('mounts deprecated unstable_extraMiddleware before unstable_priorityMiddleware', async () => { + const calls = []; + await startServer(await loadConfig(), { + unstable_extraMiddleware: [ + (req, res, next) => { + calls.push('extra'); + next(); + }, + ], + unstable_priorityMiddleware: [ + (req, res, next) => { + calls.push('framework priority'); + next(); + }, + ], + }); + + expect((await request(BUNDLE_PATH)).status).toBe(200); + expect(calls).toEqual(['extra', 'framework priority']); + }); + + test('mounts entries with a path using connect semantics', async () => { + const config = await loadConfig({ + unstable_priorityMiddleware: [ + [ + '/custom', + (req, res) => { + res.end(`${req.url} ${req.originalUrl}`); + }, + ], + ], + }); + await startServer(config); + + expect((await request('/custom/a')).text).toBe('/a /custom/a'); + expect((await request(BUNDLE_PATH)).status).toBe(200); + }); + + test('calls config, then runServer unstable_onServerCreated, before listening', async () => { + const events = []; + await startServer( + await loadConfig({ + unstable_onServerCreated: metroServer => { + events.push(['config', metroServer instanceof MetroServer]); + }, + }), + { + unstable_onServerCreated: metroServer => { + events.push(['runServer', metroServer instanceof MetroServer]); + }, + onReady: () => { + events.push('ready'); + }, + }, + ); + + expect(events).toEqual([['config', true], ['runServer', true], 'ready']); + }); + + test('does not warn for the default server.enhanceMiddleware', async () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + await startServer(await loadConfig()); + + expect(warn).not.toHaveBeenCalled(); + warn.mockRestore(); + }); + + test('warns when server.enhanceMiddleware is set', async () => { + const warn = jest.spyOn(console, 'warn').mockImplementation(() => {}); + await startServer( + await loadConfig({enhanceMiddleware: middleware => middleware}), + ); + + expect(warn).toHaveBeenCalledWith( + expect.stringContaining('DEPRECATED'), + expect.stringContaining('`server.enhanceMiddleware`'), + ); + expect((await request(BUNDLE_PATH)).status).toBe(200); + warn.mockRestore(); + }); +});