Skip to content

Commit 85b395f

Browse files
authored
feat: add a programmatic dev server API (#1118)
1 parent a5d8375 commit 85b395f

21 files changed

+1302
-2
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "@react-native-community/cli-server-api",
3+
"version": "0.0.0",
4+
"license": "MIT",
5+
"main": "build/index.js",
6+
"publishConfig": {
7+
"access": "public"
8+
},
9+
"dependencies": {
10+
"@react-native-community/cli-debugger-ui": "^4.2.1",
11+
"@react-native-community/cli-tools": "^4.7.0",
12+
"compression": "^1.7.1",
13+
"connect": "^3.6.5",
14+
"errorhandler": "^1.5.0",
15+
"pretty-format": "^25.1.0",
16+
"serve-static": "^1.13.1",
17+
"ws": "^1.1.0"
18+
},
19+
"devDependencies": {
20+
"@types/compression": "^1.0.1",
21+
"@types/connect": "^3.4.33",
22+
"@types/errorhandler": "^0.0.32",
23+
"@types/ws": "^6.0.3"
24+
},
25+
"files": [
26+
"build",
27+
"!*.d.ts",
28+
"!*.map"
29+
]
30+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
import http from 'http';
8+
import {launchDebugger, logger} from '@react-native-community/cli-tools';
9+
import {exec} from 'child_process';
10+
11+
function launchDefaultDebugger(
12+
host: string | undefined,
13+
port: number,
14+
args = '',
15+
) {
16+
const hostname = host || 'localhost';
17+
const debuggerURL = `http://${hostname}:${port}/debugger-ui${args}`;
18+
logger.info('Launching Dev Tools...');
19+
launchDebugger(debuggerURL);
20+
}
21+
22+
function escapePath(pathname: string) {
23+
// " Can escape paths with spaces in OS X, Windows, and *nix
24+
return `"${pathname}"`;
25+
}
26+
27+
type LaunchDevToolsOptions = {
28+
host?: string;
29+
port: number;
30+
watchFolders: Array<string>;
31+
};
32+
33+
function launchDevTools(
34+
{host, port, watchFolders}: LaunchDevToolsOptions,
35+
isDebuggerConnected: () => boolean,
36+
) {
37+
// Explicit config always wins
38+
const customDebugger = process.env.REACT_DEBUGGER;
39+
if (customDebugger) {
40+
startCustomDebugger({watchFolders, customDebugger});
41+
} else if (!isDebuggerConnected()) {
42+
// Debugger is not yet open; we need to open a session
43+
launchDefaultDebugger(host, port);
44+
}
45+
}
46+
47+
function startCustomDebugger({
48+
watchFolders,
49+
customDebugger,
50+
}: {
51+
watchFolders: Array<string>;
52+
customDebugger: string;
53+
}) {
54+
const folders = watchFolders.map(escapePath).join(' ');
55+
const command = `${customDebugger} ${folders}`;
56+
logger.info('Starting custom debugger by executing:', command);
57+
exec(command, function(error) {
58+
if (error !== null) {
59+
logger.error('Error while starting custom debugger:', error.stack || '');
60+
}
61+
});
62+
}
63+
64+
export default function getDevToolsMiddleware(
65+
options: LaunchDevToolsOptions,
66+
isDebuggerConnected: () => boolean,
67+
) {
68+
return function devToolsMiddleware(
69+
_req: http.IncomingMessage,
70+
res: http.ServerResponse,
71+
) {
72+
launchDevTools(options, isDebuggerConnected);
73+
res.end('OK');
74+
};
75+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>React Native</title>
5+
</head>
6+
<body>
7+
<p>React Native packager is running.</p>
8+
<p><a href="https://reactnative.dev">Visit documentation</a></p>
9+
</body>
10+
</html>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import http, {Server as HttpServer} from 'http';
2+
import {Server as HttpsServer} from 'https';
3+
4+
import compression from 'compression';
5+
import connect from 'connect';
6+
import errorhandler from 'errorhandler';
7+
import serveStatic from 'serve-static';
8+
import {debuggerUIMiddleware} from '@react-native-community/cli-debugger-ui';
9+
10+
import devToolsMiddleware from './devToolsMiddleware';
11+
import indexPageMiddleware from './indexPageMiddleware';
12+
import openStackFrameInEditorMiddleware from './openStackFrameInEditorMiddleware';
13+
import openURLMiddleware from './openURLMiddleware';
14+
import rawBodyMiddleware from './rawBodyMiddleware';
15+
import securityHeadersMiddleware from './securityHeadersMiddleware';
16+
import statusPageMiddleware from './statusPageMiddleware';
17+
import systraceProfileMiddleware from './systraceProfileMiddleware';
18+
19+
import debuggerProxyServer from './websocket/debuggerProxyServer';
20+
import eventsSocketServer from './websocket/eventsSocketServer';
21+
import messageSocketServer from './websocket/messageSocketServer';
22+
23+
export {devToolsMiddleware};
24+
export {indexPageMiddleware};
25+
export {openStackFrameInEditorMiddleware};
26+
export {openURLMiddleware};
27+
export {rawBodyMiddleware};
28+
export {securityHeadersMiddleware};
29+
export {statusPageMiddleware};
30+
export {systraceProfileMiddleware};
31+
32+
export {debuggerProxyServer};
33+
export {eventsSocketServer};
34+
export {messageSocketServer};
35+
36+
type MiddlewareOptions = {
37+
host?: string;
38+
watchFolders: Array<string>;
39+
port: number;
40+
};
41+
42+
export function createDevServerMiddleware(options: MiddlewareOptions) {
43+
let isDebuggerConnected = () => false;
44+
let broadcast = (_event: any) => {};
45+
46+
const middleware = connect()
47+
.use(securityHeadersMiddleware)
48+
// @ts-ignore compression and connect types mismatch
49+
.use(compression())
50+
.use('/debugger-ui', debuggerUIMiddleware())
51+
.use(
52+
'/launch-js-devtools',
53+
devToolsMiddleware(options, () => isDebuggerConnected()),
54+
)
55+
.use('/open-stack-frame', openStackFrameInEditorMiddleware(options))
56+
.use('/open-url', openURLMiddleware)
57+
.use('/status', statusPageMiddleware)
58+
.use('/systrace', systraceProfileMiddleware)
59+
.use('/reload', (_req: http.IncomingMessage, res: http.ServerResponse) => {
60+
broadcast('reload');
61+
res.end('OK');
62+
})
63+
.use(errorhandler());
64+
65+
options.watchFolders.forEach(folder => {
66+
// @ts-ignore mismatch between express and connect middleware types
67+
middleware.use(serveStatic(folder));
68+
});
69+
70+
return {
71+
attachToServer(server: HttpServer | HttpsServer) {
72+
const debuggerProxy = debuggerProxyServer.attachToServer(
73+
server,
74+
'/debugger-proxy',
75+
);
76+
const messageSocket = messageSocketServer.attachToServer(
77+
server,
78+
'/message',
79+
);
80+
broadcast = messageSocket.broadcast;
81+
const eventsSocket = eventsSocketServer.attachToServer(
82+
server,
83+
'/events',
84+
messageSocket,
85+
);
86+
return {
87+
debuggerProxy,
88+
eventsSocket,
89+
messageSocket,
90+
};
91+
},
92+
middleware,
93+
};
94+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
import http from 'http';
8+
import fs from 'fs';
9+
import path from 'path';
10+
11+
export default function indexPageMiddleware(
12+
req: http.IncomingMessage,
13+
res: http.ServerResponse,
14+
next: (err?: any) => void,
15+
) {
16+
if (req.url === '/') {
17+
res.setHeader('Content-Type', 'text/html');
18+
res.end(fs.readFileSync(path.join(__dirname, 'index.html')));
19+
} else {
20+
next();
21+
}
22+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
import http from 'http';
8+
import {launchEditor} from '@react-native-community/cli-tools';
9+
import connect from 'connect';
10+
import rawBodyMiddleware from './rawBodyMiddleware';
11+
12+
type Options = {
13+
watchFolders: Array<string>;
14+
};
15+
16+
function getOpenStackFrameInEditorMiddleware({watchFolders}: Options) {
17+
return (
18+
req: http.IncomingMessage & {rawBody?: string},
19+
res: http.ServerResponse,
20+
next: (err?: any) => void,
21+
) => {
22+
if (!req.rawBody) {
23+
return next(new Error('missing request body'));
24+
}
25+
const frame = JSON.parse(req.rawBody);
26+
launchEditor(frame.file, frame.lineNumber, watchFolders);
27+
res.end('OK');
28+
};
29+
}
30+
31+
export default (options: Options) => {
32+
return connect()
33+
.use(rawBodyMiddleware)
34+
.use(getOpenStackFrameInEditorMiddleware(options));
35+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
import http from 'http';
8+
import {launchDefaultBrowser, logger} from '@react-native-community/cli-tools';
9+
import connect from 'connect';
10+
import rawBodyMiddleware from './rawBodyMiddleware';
11+
12+
/**
13+
* Handle request from JS to open an arbitrary URL in Chrome
14+
*/
15+
function openURLMiddleware(
16+
req: http.IncomingMessage & {rawBody?: string},
17+
res: http.ServerResponse,
18+
next: (err?: any) => void,
19+
) {
20+
if (!req.rawBody) {
21+
return next(new Error('missing request body'));
22+
}
23+
const {url} = JSON.parse(req.rawBody);
24+
logger.info(`Opening ${url}...`);
25+
launchDefaultBrowser(url);
26+
res.end('OK');
27+
}
28+
29+
export default connect()
30+
.use(rawBodyMiddleware)
31+
.use(openURLMiddleware);
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
import http from 'http';
8+
9+
export default function rawBodyMiddleware(
10+
req: http.IncomingMessage,
11+
_res: http.ServerResponse,
12+
next: (err?: any) => void,
13+
) {
14+
(req as http.IncomingMessage & {rawBody: string}).rawBody = '';
15+
req.setEncoding('utf8');
16+
17+
req.on('data', (chunk: string) => {
18+
(req as http.IncomingMessage & {rawBody: string}).rawBody += chunk;
19+
});
20+
21+
req.on('end', () => {
22+
next();
23+
});
24+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
import http from 'http';
8+
9+
export default function securityHeadersMiddleware(
10+
req: http.IncomingMessage,
11+
res: http.ServerResponse,
12+
next: (err?: any) => void,
13+
) {
14+
// Block any cross origin request.
15+
if (
16+
typeof req.headers.origin === 'string' &&
17+
!req.headers.origin.match(/^https?:\/\/localhost:/)
18+
) {
19+
next(
20+
new Error(
21+
'Unauthorized request from ' +
22+
req.headers.origin +
23+
'. This may happen because of a conflicting browser extension. Please try to disable it and try again.',
24+
),
25+
);
26+
return;
27+
}
28+
29+
// Block MIME-type sniffing.
30+
res.setHeader('X-Content-Type-Options', 'nosniff');
31+
32+
next();
33+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
import http from 'http';
8+
9+
/**
10+
* Status page so that anyone who needs to can verify that the packager is
11+
* running on 8081 and not another program / service.
12+
*/
13+
export default function statusPageMiddleware(
14+
_req: http.IncomingMessage,
15+
res: http.ServerResponse,
16+
) {
17+
res.end('packager-status:running');
18+
}

0 commit comments

Comments
 (0)