Skip to content

Commit 35ff94b

Browse files
vzaidmanmeta-codesync[bot]
authored andcommitted
make metro accept tls server config (#1657)
Summary: X-link: facebook/react-native#55701 Pull Request resolved: #1657 Changelog: [Feature] `config.server.tls` now sets Metro to be exposed as an https server Reviewed By: robhogan, huntie Differential Revision: D93857257 fbshipit-source-id: 56ff661c4ddf9cd5d4bb32756b9cb600bb032a1c
1 parent 34ca6a6 commit 35ff94b

File tree

11 files changed

+846
-13
lines changed

11 files changed

+846
-13
lines changed

docs/Configuration.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,25 @@ Type: `boolean`
657657
658658
Enable forwarding of `client_log` events (when client logs are [configured](https://github.com/facebook/metro/blob/614ad14a85b22958129ee94e04376b096f03ccb1/packages/metro/src/lib/createWebsocketServer.js#L20)) to the reporter. Defaults to `true`.
659659
660+
#### `tls`
661+
662+
Type: `false | object`
663+
664+
If not provided or is `false` Metro will start an HTTP server with WS WebSocket endpoints.
665+
666+
If an object, Metro will start an HTTPS server with WSS WebSocket endpoints using the passed TLS options:
667+
668+
```ts
669+
ca?: string | Buffer, // Certificate authority (contents, not path)
670+
cert?: string | Buffer, // Server certificate (contents, not path)
671+
key?: string | Buffer, // Private key (contents, not path)
672+
requestCert?: boolean, // Whether to authenticate the remote peer by requesting a certificate
673+
```
674+
675+
Notice that when overriding the base config, object tls configs extend the base tls config, false overrides the base tls configs, and `null` and `undefined` are ignored.
676+
677+
When running Metro with `Metro.runServer` with the `secureServerOptions` property Metro will likewise start an HTTPS server merging with the `config.server.tls` object if provided, overriding it.
678+
660679
---
661680
662681
### Watcher Options

packages/metro-config/src/__tests__/mergeConfig-test.js

Lines changed: 167 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @flow strict-local
7+
* @flow
88
* @format
99
* @oncall react_native
1010
*/
1111

12+
import type {InputConfigT} from '../types';
13+
1214
import {mergeConfig} from '../loadConfig';
1315

1416
describe('mergeConfig', () => {
@@ -26,4 +28,168 @@ describe('mergeConfig', () => {
2628
},
2729
});
2830
});
31+
32+
describe('server.tls merging', () => {
33+
describe('override IS applied when tls is false or object', () => {
34+
test('override tls: object replaces base tls: false', () => {
35+
const base: InputConfigT = {server: {tls: false}};
36+
const override: InputConfigT = {
37+
server: {tls: {key: 'key', cert: 'cert'}},
38+
};
39+
const result = mergeConfig(base, override);
40+
expect(result.server?.tls).toStrictEqual({key: 'key', cert: 'cert'});
41+
});
42+
43+
test('override tls: false replaces base tls: object', () => {
44+
const base: InputConfigT = {server: {tls: {key: 'key', cert: 'cert'}}};
45+
const override: InputConfigT = {server: {tls: false}};
46+
const result = mergeConfig(base, override);
47+
expect(result.server?.tls).toBe(false);
48+
});
49+
50+
test('override tls: false sets tls when base is undefined', () => {
51+
const base: InputConfigT = {server: {}};
52+
const override: InputConfigT = {server: {tls: false}};
53+
const result = mergeConfig(base, override);
54+
expect(result.server?.tls).toBe(false);
55+
});
56+
57+
test('override tls: object sets tls when base is undefined', () => {
58+
const base: InputConfigT = {server: {}};
59+
const override: InputConfigT = {
60+
server: {tls: {key: 'key', cert: 'cert'}},
61+
};
62+
const result = mergeConfig(base, override);
63+
expect(result.server?.tls).toStrictEqual({key: 'key', cert: 'cert'});
64+
});
65+
66+
test('override tls: object deep merges with base tls: object', () => {
67+
const base: InputConfigT = {
68+
server: {tls: {key: 'baseKey', cert: 'baseCert', ca: 'baseCa'}},
69+
};
70+
const override: InputConfigT = {
71+
server: {tls: {key: 'newKey', cert: 'newCert'}},
72+
};
73+
const result = mergeConfig(base, override);
74+
expect(result.server?.tls).toStrictEqual({
75+
key: 'newKey',
76+
cert: 'newCert',
77+
ca: 'baseCa',
78+
});
79+
});
80+
81+
test('override tls: object adds new properties to base tls: object', () => {
82+
const base: InputConfigT = {
83+
server: {tls: {key: 'baseKey', cert: 'baseCert'}},
84+
};
85+
const override: InputConfigT = {
86+
server: {tls: {ca: 'newCa'}},
87+
};
88+
const result = mergeConfig(base, override);
89+
expect(result.server?.tls).toStrictEqual({
90+
key: 'baseKey',
91+
cert: 'baseCert',
92+
ca: 'newCa',
93+
});
94+
});
95+
96+
test('override tls: object with same properties overrides base values', () => {
97+
const base: InputConfigT = {
98+
server: {tls: {key: 'baseKey', cert: 'baseCert'}},
99+
};
100+
const override: InputConfigT = {
101+
server: {tls: {key: 'newKey', cert: 'newCert'}},
102+
};
103+
const result = mergeConfig(base, override);
104+
expect(result.server?.tls).toStrictEqual({
105+
key: 'newKey',
106+
cert: 'newCert',
107+
});
108+
});
109+
110+
test('other server properties are preserved when tls is overridden', () => {
111+
const base: InputConfigT = {server: {port: 8081, tls: false}};
112+
const override: InputConfigT = {
113+
server: {tls: {key: 'key', cert: 'cert'}},
114+
};
115+
const result = mergeConfig(base, override);
116+
expect(result.server).toStrictEqual({
117+
port: 8081,
118+
tls: {key: 'key', cert: 'cert'},
119+
});
120+
});
121+
122+
test('override tls: null replaces base tls: undefined', () => {
123+
const base: InputConfigT = {server: {}};
124+
// $FlowExpectedError[incompatible-type] - testing untyped runtime behavior
125+
const override: InputConfigT = {server: {tls: null}};
126+
const result = mergeConfig(base, override);
127+
expect(result.server?.tls).toBe(null);
128+
});
129+
});
130+
131+
describe('override is NOT applied when tls is null or undefined', () => {
132+
test('override tls: undefined keeps base tls: object', () => {
133+
const base: InputConfigT = {server: {tls: {key: 'key', cert: 'cert'}}};
134+
const override: InputConfigT = {server: {}};
135+
const result = mergeConfig(base, override);
136+
expect(result.server?.tls).toStrictEqual({key: 'key', cert: 'cert'});
137+
});
138+
139+
test('override tls: undefined (explicit) keeps base tls: object', () => {
140+
const base: InputConfigT = {server: {tls: {key: 'key', cert: 'cert'}}};
141+
// $FlowExpectedError[incompatible-type] - testing explicit undefined
142+
const override: InputConfigT = {server: {tls: undefined}};
143+
const result = mergeConfig(base, override);
144+
expect(result.server?.tls).toStrictEqual({key: 'key', cert: 'cert'});
145+
});
146+
147+
test('override tls: undefined keeps base tls: false', () => {
148+
const base: InputConfigT = {server: {tls: false}};
149+
const override: InputConfigT = {server: {}};
150+
const result = mergeConfig(base, override);
151+
expect(result.server?.tls).toBe(false);
152+
});
153+
154+
test('override tls: undefined (explicit) keeps base tls: false', () => {
155+
const base: InputConfigT = {server: {tls: false}};
156+
// $FlowExpectedError[incompatible-type] - testing untyped runtime behavior
157+
const override: InputConfigT = {server: {tls: undefined}};
158+
const result = mergeConfig(base, override);
159+
expect(result.server?.tls).toBe(false);
160+
});
161+
162+
test('override tls: null keeps base tls: object', () => {
163+
const base: InputConfigT = {server: {tls: {key: 'key', cert: 'cert'}}};
164+
// $FlowExpectedError[incompatible-type] - testing untyped runtime behavior
165+
const override: InputConfigT = {server: {tls: null}};
166+
const result = mergeConfig(base, override);
167+
expect(result.server?.tls).toStrictEqual({key: 'key', cert: 'cert'});
168+
});
169+
170+
test('override tls: null keeps base tls: false', () => {
171+
const base: InputConfigT = {server: {tls: false}};
172+
// $FlowExpectedError[incompatible-type] - testing untyped runtime behavior
173+
const override: InputConfigT = {server: {tls: null}};
174+
const result = mergeConfig(base, override);
175+
expect(result.server?.tls).toBe(false);
176+
});
177+
178+
test('both tls undefined results in no tls property', () => {
179+
const base: InputConfigT = {server: {}};
180+
const override: InputConfigT = {server: {}};
181+
const result = mergeConfig(base, override);
182+
expect(result.server?.tls).toBeUndefined();
183+
});
184+
185+
test('both tls undefined (explicit) results in no tls property', () => {
186+
// $FlowExpectedError[incompatible-type] - testing untyped runtime behavior
187+
const base: InputConfigT = {server: {tls: undefined}};
188+
// $FlowExpectedError[incompatible-type] - testing untyped runtime behavior
189+
const override: InputConfigT = {server: {tls: undefined}};
190+
const result = mergeConfig(base, override);
191+
expect(result.server?.tls).toBeUndefined();
192+
});
193+
});
194+
});
29195
});

packages/metro-config/src/defaults/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ const getDefaultValues = (projectRoot: ?string): ConfigT => ({
8080
unstable_serverRoot: null,
8181
useGlobalHotkey: true,
8282
verifyConnections: false,
83+
tls: false,
8384
},
8485

8586
symbolicator: {

packages/metro-config/src/loadConfig.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,21 @@ function mergeConfigObjects<T: InputConfigT>(
152152
},
153153
server: {
154154
...base.server,
155-
// $FlowFixMe[exponential-spread]
156155
...overrides.server,
156+
// $FlowFixMe[exponential-spread]
157+
...(base.server?.tls != null ? {tls: base.server?.tls} : null),
158+
// only override base tls config with false or an object
159+
...(overrides.server?.tls === false
160+
? {tls: false}
161+
: overrides.server?.tls != null &&
162+
typeof overrides.server?.tls === 'object'
163+
? {
164+
tls: {
165+
...(base.server?.tls || {}),
166+
...overrides.server?.tls,
167+
},
168+
}
169+
: null),
157170
},
158171
symbolicator: {
159172
...base.symbolicator,

packages/metro-config/src/types.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,14 @@ type ServerConfigT = {
184184
unstable_serverRoot: ?string,
185185
useGlobalHotkey: boolean,
186186
verifyConnections: boolean,
187+
tls:
188+
| false
189+
| {
190+
ca?: string | Buffer,
191+
cert?: string | Buffer,
192+
key?: string | Buffer,
193+
requestCert?: boolean,
194+
},
187195
};
188196

189197
type SymbolicatorConfigT = {

packages/metro-config/types/types.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,14 @@ type ServerConfigT = {
179179
unstable_serverRoot: null | undefined | string;
180180
useGlobalHotkey: boolean;
181181
verifyConnections: boolean;
182+
tls:
183+
| false
184+
| {
185+
ca?: string | Buffer;
186+
cert?: string | Buffer;
187+
key?: string | Buffer;
188+
requestCert?: boolean;
189+
};
182190
};
183191
type SymbolicatorConfigT = {
184192
customizeFrame: ($$PARAM_0$$: {

packages/metro/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"metro-memory-fs": "*",
7777
"mock-req": "^0.2.0",
7878
"mock-res": "^0.6.0",
79+
"selfsigned": "^5.5.0",
7980
"stack-trace": "^0.0.10"
8081
},
8182
"license": "MIT",

packages/metro/src/index.flow.js

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ export const runServer = async (
280280
chalk.inverse.yellow.bold(' DEPRECATED '),
281281
'The `secure`, `secureCert`, and `secureKey` options are now deprecated. ' +
282282
'Please use the `secureServerOptions` object instead to pass options to ' +
283-
"Metro's https development server.",
283+
"Metro's https development server, or `config.server.tls` in Metro's configuration",
284284
);
285285
}
286286
// Lazy require
@@ -307,15 +307,30 @@ export const runServer = async (
307307

308308
let httpServer;
309309

310-
if (secure === true || secureServerOptions != null) {
311-
let options = secureServerOptions;
312-
if (typeof secureKey === 'string' && typeof secureCert === 'string') {
313-
options = {
314-
key: fs.readFileSync(secureKey),
315-
cert: fs.readFileSync(secureCert),
316-
...secureServerOptions,
317-
};
318-
}
310+
const {tls} = config.server;
311+
if (
312+
secure === true ||
313+
secureServerOptions != null ||
314+
typeof tls === 'object'
315+
) {
316+
const options = {
317+
key:
318+
typeof tls === 'object'
319+
? tls.key
320+
: typeof secureKey === 'string'
321+
? fs.readFileSync(secureKey)
322+
: undefined,
323+
cert:
324+
typeof tls === 'object'
325+
? tls.cert
326+
: typeof secureCert === 'string'
327+
? fs.readFileSync(secureCert)
328+
: undefined,
329+
ca: typeof tls === 'object' ? tls.ca : undefined,
330+
requestCert: typeof tls === 'object' ? tls.requestCert : undefined,
331+
...(secureServerOptions ?? {}),
332+
};
333+
319334
// $FlowFixMe[incompatible-type] 'http' and 'https' Flow types do not match
320335
httpServer = https.createServer(options, serverApp);
321336
} else {

0 commit comments

Comments
 (0)