Skip to content
Open
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
4 changes: 2 additions & 2 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,11 @@ Bundles `entry` for the given `platform`, and saves it to location `out`. If `so

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

**Basic options:** `host`, `port`, `secureServerOptions`, `unstable_middleware`, `unstable_priorityMiddleware`, `unstable_onServerCreated`, `secure (DEPRECATED)`, `secureKey (DEPRECATED)`, `secureCert (DEPRECATED)`
**Basic options:** `host`, `port`, `secureServerOptions`, `unstable_middleware`, `unstable_priorityMiddleware`, `websocketEndpoints`, `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.
`unstable_middleware`, `unstable_priorityMiddleware` and `websocketEndpoints` take the same shapes as [`server.unstable_middleware`](./Configuration.md#unstable_middleware-experimental), [`server.unstable_priorityMiddleware`](./Configuration.md#unstable_prioritymiddleware-experimental) and [`server.unstable_websocketEndpoints`](./Configuration.md#unstable_websocketendpoints-experimental), and wrap them: `unstable_middleware` passed here runs after the config's, and `unstable_priorityMiddleware` runs before the config's. For the same websocket path, `websocketEndpoints` passed here win over those from the config. [`/hot`](./Configuration.md#unstable_websocketendpoints-experimental) is reserved for Metro.

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.

Expand Down
12 changes: 11 additions & 1 deletion docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,16 @@ Called with Metro's server instance when [`runServer`](./API.md#async-runserverc

`runServer` also takes an `unstable_onServerCreated` option, which is called after this one.

#### `unstable_websocketEndpoints` <div class="label experimental">Experimental</div>

Type: `{[path: string]: WebSocketServer}`

[`ws`](https://www.npmjs.com/package/ws) servers, created with `noServer: true`, to handle WebSocket upgrades on the given paths.

`/hot` is reserved for Metro's hot reloading. A supplied `/hot` endpoint is ignored, with a warning.

[`runServer`](./API.md#async-runserverconfig-options) also takes a `websocketEndpoints` option, which wins over these for the same path.

#### `rewriteRequestUrl`

Type: `string => string`
Expand Down Expand Up @@ -868,7 +878,7 @@ 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.
`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. `server.unstable_websocketEndpoints` are combined too; for the same path, a later config's server wins.

#### Merging Example

Expand Down
2 changes: 2 additions & 0 deletions packages/metro-config/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import EventEmitter from 'node:events';
import type {HandleFunction} from 'connect';
import type {IncomingMessage} from 'connect';
import type {Server as Server_2} from 'connect';
import type {Server as Server_3} from 'ws';
import type {ServerResponse} from 'node:http';

export type CacheStoresConfigT = ReadonlyArray<CacheStore<TransformResult>>;
Expand Down Expand Up @@ -191,6 +192,7 @@ export type ServerConfigT = {
unstable_onServerCreated: (metroServer: Server) => void;
unstable_priorityMiddleware: ReadonlyArray<ServerMiddleware>;
unstable_serverRoot: null | undefined | string;
unstable_websocketEndpoints: Readonly<{[path: string]: Server_3}>;
useGlobalHotkey: boolean;
verifyConnections: boolean;
tls:
Expand Down
65 changes: 65 additions & 0 deletions packages/metro-config/src/__tests__/mergeConfig-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import type {InputConfigT, ServerMiddleware} from '../types';
import type {CustomResolver} from 'metro-resolver';
import type {Server as WebSocketServer} from 'ws';

import {mergeConfig} from '../loadConfig';
import path from 'node:path';
Expand Down Expand Up @@ -235,6 +236,70 @@ describe('mergeConfig', () => {
});
});

describe('server websocket endpoint merging', () => {
const a: WebSocketServer = {} as $FlowFixMe;
const b: WebSocketServer = {} as $FlowFixMe;

test('combines endpoints from both configs', () => {
const result = mergeConfig(
{server: {unstable_websocketEndpoints: {'/a': a}}},
{server: {unstable_websocketEndpoints: {'/b': b}}},
);
expect(result.server?.unstable_websocketEndpoints).toEqual({
'/a': a,
'/b': b,
});
});

test('keeps endpoints defined by only one config', () => {
const withA: InputConfigT = {
server: {unstable_websocketEndpoints: {'/a': a}},
};
const withB: InputConfigT = {
server: {unstable_websocketEndpoints: {'/b': b}},
};
const empty: InputConfigT = {server: {}};
expect(
mergeConfig(withA, empty).server?.unstable_websocketEndpoints,
).toEqual({'/a': a});
expect(
mergeConfig(empty, withB).server?.unstable_websocketEndpoints,
).toEqual({'/b': b});
});

test('keeps base endpoints when the override sets them to undefined', () => {
const withA: InputConfigT = {
server: {unstable_websocketEndpoints: {'/a': a}},
};
const explicitUndefined: InputConfigT = {
server: {unstable_websocketEndpoints: undefined},
};
expect(
mergeConfig(withA, explicitUndefined).server
?.unstable_websocketEndpoints,
).toEqual({'/a': a});
});

test('keeps an endpoint copied from the base config', () => {
const result = mergeConfig(
{server: {unstable_websocketEndpoints: {'/a': a}}},
{server: {unstable_websocketEndpoints: {'/a': a, '/b': b}}},
);
expect(result.server?.unstable_websocketEndpoints).toEqual({
'/a': a,
'/b': b,
});
});

test('a later config wins for the same path', () => {
const result = mergeConfig(
{server: {unstable_websocketEndpoints: {'/a': a}}},
{server: {unstable_websocketEndpoints: {'/a': b}}},
);
expect(result.server?.unstable_websocketEndpoints).toEqual({'/a': b});
});
});

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
1 change: 1 addition & 0 deletions packages/metro-config/src/defaults/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const getDefaultValues = (projectRoot: ?string): ConfigT => ({
// merged with user middleware as [...user, ...defaults]
unstable_priorityMiddleware: [],
unstable_serverRoot: null,
unstable_websocketEndpoints: {},
useGlobalHotkey: true,
verifyConnections: false,
tls: false,
Expand Down
21 changes: 18 additions & 3 deletions packages/metro-config/src/loadConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
ServerMiddleware,
YargArguments,
} from './types';
import type {Server as WebSocketServer} from 'ws';

import getDefaultConfig from './defaults';
import validConfig from './defaults/validConfig';
Expand Down Expand Up @@ -201,18 +202,21 @@ function mergeConfigObjects<T extends InputConfigT>(
};
}

// Later configs wrap earlier ones: their `unstable_middleware` runs last and
// their `unstable_priorityMiddleware` runs first.
// Later configs wrap earlier ones: their `unstable_middleware` runs last, their
// `unstable_priorityMiddleware` runs first, and their websocket endpoints take
// precedence.
function mergeServerHandlers(
base: InputConfigT['server'],
overrides: InputConfigT['server'],
): {
unstable_middleware?: ReadonlyArray<ServerMiddleware>,
unstable_priorityMiddleware?: ReadonlyArray<ServerMiddleware>,
unstable_websocketEndpoints?: Readonly<{[path: string]: WebSocketServer}>,
} {
const merged: {
unstable_middleware?: ReadonlyArray<ServerMiddleware>,
unstable_priorityMiddleware?: ReadonlyArray<ServerMiddleware>,
unstable_websocketEndpoints?: Readonly<{[path: string]: WebSocketServer}>,
} = {};
const middleware = concatUnique(
base?.unstable_middleware,
Expand All @@ -228,6 +232,12 @@ function mergeServerHandlers(
if (priorityMiddleware != null) {
merged.unstable_priorityMiddleware = priorityMiddleware;
}
const baseEndpoints = base?.unstable_websocketEndpoints;
const overrideEndpoints = overrides?.unstable_websocketEndpoints;
if (baseEndpoints != null || overrideEndpoints != null) {
const fromBase: {[path: string]: WebSocketServer} = {...baseEndpoints};
merged.unstable_websocketEndpoints = {...fromBase, ...overrideEndpoints};
}
return merged;
}

Expand Down Expand Up @@ -425,7 +435,12 @@ async function loadConfig(

validate(configuration, {
exampleConfig: await validConfig(),
recursiveDenylist: ['reporter', 'resolver', 'transformer'],
recursiveDenylist: [
'reporter',
'resolver',
'transformer',
'server.unstable_websocketEndpoints',
],
deprecatedConfig: {},
});

Expand Down
4 changes: 4 additions & 0 deletions packages/metro-config/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
import type {Reporter} from 'metro/private/lib/reporting';
import type MetroServer from 'metro/private/Server';
import type {IntermediateStackFrame} from 'metro/private/Server/symbolicate';
import type {Server as WebSocketServer} from 'ws';

export type ExtraTransformOptions = Readonly<{
preloadedModules?: Readonly<{[path: string]: true, ...}> | false,
Expand Down Expand Up @@ -209,6 +210,9 @@ type ServerConfigT = {
// handlers.
unstable_priorityMiddleware: ReadonlyArray<ServerMiddleware>,
unstable_serverRoot: ?string,
// `runServer`'s `websocketEndpoints` option wins over these for the same
// path.
unstable_websocketEndpoints: Readonly<{[path: string]: WebSocketServer}>,
useGlobalHotkey: boolean,
verifyConnections: boolean,
tls:
Expand Down
12 changes: 6 additions & 6 deletions packages/metro/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import EventEmitter from 'node:events';
import type {HandleFunction} from 'connect';
import type {IncomingMessage} from 'connect';
import type {Server} from 'connect';
import type {Server as Server_2} from 'node:http';
import type {Server as Server_3} from 'node:https';
import type {Server as Server_4} from 'ws';
import type {Server as Server_2} from 'ws';
import type {Server as Server_3} from 'node:http';
import type {Server as Server_4} from 'node:https';
import type {ServerOptions as ServerOptions_2} from 'node:https';
import type {ServerResponse} from 'node:http';
import type {Socket} from 'node:net';
Expand Down Expand Up @@ -273,7 +273,7 @@ export type RunServerOptions = Readonly<{
hasReducedPerformance?: boolean | undefined;
host?: string | undefined;
onError?: ((err: Error & {code?: string | undefined}) => void) | undefined;
onReady?: ((server: Server_2 | Server_3) => void) | undefined;
onReady?: ((server: Server_3 | Server_4) => void) | undefined;
onClose?: (() => void) | undefined;
secureServerOptions?: ServerOptions_2 | undefined;
secure?: boolean | undefined;
Expand All @@ -285,10 +285,10 @@ export type RunServerOptions = Readonly<{
unstable_priorityMiddleware?: ReadonlyArray<ServerMiddleware> | undefined;
waitForBundler?: boolean | undefined;
watch?: boolean | undefined;
websocketEndpoints?: Readonly<{[path: string]: Server_4}> | undefined;
websocketEndpoints?: Readonly<{[path: string]: Server_2}> | undefined;
}>;

export type RunServerResult = {httpServer: Server_2 | Server_3};
export type RunServerResult = {httpServer: Server_3 | Server_4};

export type SerializerOptions = Readonly<{
asyncRequireModulePath: string;
Expand Down
Loading
Loading