Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,16 @@ Bundles `entry` for the given `platform`, and saves it to location `out`. If `so

### `async runServer(config, <options>)`

**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, <options>)`
Expand Down
42 changes: 41 additions & 1 deletion docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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` <div class="label experimental">Experimental</div>

Type: `Array<Middleware | [path: string, handler: Middleware]>`

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` <div class="label experimental">Experimental</div>

Type: `Array<Middleware | [path: string, handler: Middleware]>`

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` <div class="label experimental">Experimental</div>

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`
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions packages/metro-config/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ export type ServerConfigT = {
forwardClientLogs: boolean;
port: number;
rewriteRequestUrl: (url: string) => string;
unstable_middleware: ReadonlyArray<ServerMiddleware>;
unstable_onServerCreated: (metroServer: Server) => void;
unstable_priorityMiddleware: ReadonlyArray<ServerMiddleware>;
unstable_serverRoot: null | undefined | string;
useGlobalHotkey: boolean;
verifyConnections: boolean;
Expand All @@ -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;
Expand Down
32 changes: 31 additions & 1 deletion packages/metro-config/src/__tests__/mergeConfig-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion packages/metro-config/src/defaults/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -63,6 +63,10 @@ export const platforms: Array<string> = ['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() {}
Expand Down
9 changes: 8 additions & 1 deletion packages/metro-config/src/defaults/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
assetExts,
assetResolutions,
defaultCreateModuleIdFactory,
defaultEnhanceMiddleware,
noopPerfLoggerFactory,
platforms,
sourceExts,
Expand Down Expand Up @@ -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,
Expand Down
50 changes: 49 additions & 1 deletion packages/metro-config/src/loadConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -152,6 +157,7 @@ function mergeConfigObjects<T extends InputConfigT>(
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
Expand Down Expand Up @@ -195,6 +201,48 @@ function mergeConfigObjects<T extends InputConfigT>(
};
}

// 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<ServerMiddleware>,
unstable_priorityMiddleware?: ReadonlyArray<ServerMiddleware>,
} {
const merged: {
unstable_middleware?: ReadonlyArray<ServerMiddleware>,
unstable_priorityMiddleware?: ReadonlyArray<ServerMiddleware>,
} = {};
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<T>(
first: ?ReadonlyArray<T>,
second: ?ReadonlyArray<T>,
): ?ReadonlyArray<T> {
if (first == null || second == null) {
return first ?? second;
}
return [...new Set([...first, ...second])];
}

async function mergeConfigAsync<T extends InputConfigT>(
baseConfig: Promise<T>,
...reversedConfigs: ReadonlyArray<
Expand Down
18 changes: 17 additions & 1 deletion packages/metro-config/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}>,
Expand Down Expand Up @@ -184,14 +189,25 @@ type MetalConfigT = {
type CacheStoresConfigT = ReadonlyArray<CacheStore<TransformResult<>>>;

type ServerConfigT = {
/** @deprecated */
/** @deprecated Use `unstable_middleware`, `unstable_priorityMiddleware`, or `unstable_onServerCreated` */
enhanceMiddleware: (
middleware: Middleware,
server: MetroServer,
) => Middleware | Server,
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<ServerMiddleware>,
// 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<ServerMiddleware>,
unstable_serverRoot: ?string,
useGlobalHotkey: boolean,
verifyConnections: boolean,
Expand Down
3 changes: 3 additions & 0 deletions packages/metro/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ export type RunServerOptions = Readonly<{
secureCert?: string | undefined;
secureKey?: string | undefined;
unstable_extraMiddleware?: ReadonlyArray<HandleFunction> | undefined;
unstable_middleware?: ReadonlyArray<ServerMiddleware> | undefined;
unstable_onServerCreated?: ((metroServer: MetroServer) => void) | undefined;
unstable_priorityMiddleware?: ReadonlyArray<ServerMiddleware> | undefined;
waitForBundler?: boolean | undefined;
watch?: boolean | undefined;
websocketEndpoints?: Readonly<{[path: string]: Server_4}> | undefined;
Expand Down
Loading
Loading