Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 30 additions & 0 deletions packages/dev-server-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@react-native-community/dev-server-api",
"version": "4.7.0",
"license": "MIT",
"main": "build/index.js",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@react-native-community/cli-debugger-ui": "^4.2.1",
"@react-native-community/cli-tools": "^4.7.0",
"compression": "^1.7.1",
"connect": "^3.6.5",
"errorhandler": "^1.5.0",
"pretty-format": "^25.1.0",
"serve-static": "^1.13.1",
"ws": "^1.1.0"
},
"devDependencies": {
"@types/compression": "^1.0.1",
"@types/connect": "^3.4.33",
"@types/errorhandler": "^0.0.32",
"@types/ws": "^6.0.3"
},
"files": [
"build",
"!*.d.ts",
"!*.map"
]
}
75 changes: 75 additions & 0 deletions packages/dev-server-api/src/devToolsMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import http from 'http';
import {launchDebugger, logger} from '@react-native-community/cli-tools';
import {exec} from 'child_process';

function launchDefaultDebugger(
host: string | undefined,
port: number,
args = '',
) {
const hostname = host || 'localhost';
const debuggerURL = `http://${hostname}:${port}/debugger-ui${args}`;
logger.info('Launching Dev Tools...');
launchDebugger(debuggerURL);
}

function escapePath(pathname: string) {
// " Can escape paths with spaces in OS X, Windows, and *nix
return `"${pathname}"`;
}

type LaunchDevToolsOptions = {
host?: string;
port: number;
watchFolders: Array<string>;
};

function launchDevTools(
{host, port, watchFolders}: LaunchDevToolsOptions,
isDebuggerConnected: () => boolean,
) {
// Explicit config always wins
const customDebugger = process.env.REACT_DEBUGGER;
if (customDebugger) {
startCustomDebugger({watchFolders, customDebugger});
} else if (!isDebuggerConnected()) {
// Debugger is not yet open; we need to open a session
launchDefaultDebugger(host, port);
}
}

function startCustomDebugger({
watchFolders,
customDebugger,
}: {
watchFolders: Array<string>;
customDebugger: string;
}) {
const folders = watchFolders.map(escapePath).join(' ');
const command = `${customDebugger} ${folders}`;
logger.info('Starting custom debugger by executing:', command);
exec(command, function(error) {
if (error !== null) {
logger.error('Error while starting custom debugger:', error.stack || '');
}
});
}

export default function getDevToolsMiddleware(
options: LaunchDevToolsOptions,
isDebuggerConnected: () => boolean,
) {
return function devToolsMiddleware(
_req: http.IncomingMessage,
res: http.ServerResponse,
) {
launchDevTools(options, isDebuggerConnected);
res.end('OK');
};
}
10 changes: 10 additions & 0 deletions packages/dev-server-api/src/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<title>React Native</title>
</head>
<body>
<p>React Native packager is running.</p>
<p><a href="https://reactnative.dev">Visit documentation</a></p>
</body>
</html>
94 changes: 94 additions & 0 deletions packages/dev-server-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import http, {Server as HttpServer} from 'http';
import {Server as HttpsServer} from 'https';

import compression from 'compression';
import connect from 'connect';
import errorhandler from 'errorhandler';
import serveStatic from 'serve-static';
import {debuggerUIMiddleware} from '@react-native-community/cli-debugger-ui';

import devToolsMiddleware from './devToolsMiddleware';
import indexPageMiddleware from './indexPageMiddleware';
import openStackFrameInEditorMiddleware from './openStackFrameInEditorMiddleware';
import openURLMiddleware from './openURLMiddleware';
import rawBodyMiddleware from './rawBodyMiddleware';
import securityHeadersMiddleware from './securityHeadersMiddleware';
import statusPageMiddleware from './statusPageMiddleware';
import systraceProfileMiddleware from './systraceProfileMiddleware';

import debuggerProxyServer from './websocket/debuggerProxyServer';
import eventsSocketServer from './websocket/eventsSocketServer';
import messageSocketServer from './websocket/messageSocketServer';

export {devToolsMiddleware};
export {indexPageMiddleware};
export {openStackFrameInEditorMiddleware};
export {openURLMiddleware};
export {rawBodyMiddleware};
export {securityHeadersMiddleware};
export {statusPageMiddleware};
export {systraceProfileMiddleware};

export {debuggerProxyServer};
export {eventsSocketServer};
export {messageSocketServer};

type MiddlewareOptions = {
host?: string;
watchFolders: Array<string>;
port: number;
};

export function createDevServerMiddleware(options: MiddlewareOptions) {
let isDebuggerConnected = () => false;
let broadcast = (_event: any) => {};

const middleware = connect()
.use(securityHeadersMiddleware)
// @ts-ignore compression and connect types mismatch
.use(compression())
.use('/debugger-ui', debuggerUIMiddleware())
.use(
'/launch-js-devtools',
devToolsMiddleware(options, () => isDebuggerConnected()),
)
.use('/open-stack-frame', openStackFrameInEditorMiddleware(options))
.use('/open-url', openURLMiddleware)
.use('/status', statusPageMiddleware)
.use('/systrace', systraceProfileMiddleware)
.use('/reload', (_req: http.IncomingMessage, res: http.ServerResponse) => {
broadcast('reload');
res.end('OK');
})
.use(errorhandler());

options.watchFolders.forEach(folder => {
// @ts-ignore mismatch between express and connect middleware types
middleware.use(serveStatic(folder));
});

return {
attachToServer(server: HttpServer | HttpsServer) {
const debuggerProxy = debuggerProxyServer.attachToServer(
server,
'/debugger-proxy',
);
const messageSocket = messageSocketServer.attachToServer(
server,
'/message',
);
broadcast = messageSocket.broadcast;
const eventsSocket = eventsSocketServer.attachToServer(
server,
'/events',
messageSocket,
);
return {
debuggerProxy,
eventsSocket,
messageSocket,
};
},
middleware,
};
}
22 changes: 22 additions & 0 deletions packages/dev-server-api/src/indexPageMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import http from 'http';
import fs from 'fs';
import path from 'path';

export default function indexPageMiddleware(
req: http.IncomingMessage,
res: http.ServerResponse,
next: (err?: any) => void,
) {
if (req.url === '/') {
res.setHeader('Content-Type', 'text/html');
res.end(fs.readFileSync(path.join(__dirname, 'index.html')));
} else {
next();
}
}
35 changes: 35 additions & 0 deletions packages/dev-server-api/src/openStackFrameInEditorMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import http from 'http';
import {launchEditor} from '@react-native-community/cli-tools';
import connect from 'connect';
import rawBodyMiddleware from './rawBodyMiddleware';

type Options = {
watchFolders: Array<string>;
};

function getOpenStackFrameInEditorMiddleware({watchFolders}: Options) {
return (
req: http.IncomingMessage & {rawBody?: string},
res: http.ServerResponse,
next: (err?: any) => void,
) => {
if (!req.rawBody) {
return next(new Error('missing request body'));
}
const frame = JSON.parse(req.rawBody);
launchEditor(frame.file, frame.lineNumber, watchFolders);
res.end('OK');
};
}

export default (options: Options) => {
return connect()
.use(rawBodyMiddleware)
.use(getOpenStackFrameInEditorMiddleware(options));
};
31 changes: 31 additions & 0 deletions packages/dev-server-api/src/openURLMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import http from 'http';
import {launchDefaultBrowser, logger} from '@react-native-community/cli-tools';
import connect from 'connect';
import rawBodyMiddleware from './rawBodyMiddleware';

/**
* Handle request from JS to open an arbitrary URL in Chrome
*/
function openURLMiddleware(
req: http.IncomingMessage & {rawBody?: string},
res: http.ServerResponse,
next: (err?: any) => void,
) {
if (!req.rawBody) {
return next(new Error('missing request body'));
}
const {url} = JSON.parse(req.rawBody);
logger.info(`Opening ${url}...`);
launchDefaultBrowser(url);
res.end('OK');
}

export default connect()
.use(rawBodyMiddleware)
.use(openURLMiddleware);
24 changes: 24 additions & 0 deletions packages/dev-server-api/src/rawBodyMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import http from 'http';

export default function rawBodyMiddleware(
req: http.IncomingMessage,
_res: http.ServerResponse,
next: (err?: any) => void,
) {
(req as http.IncomingMessage & {rawBody: string}).rawBody = '';
req.setEncoding('utf8');

req.on('data', (chunk: string) => {
(req as http.IncomingMessage & {rawBody: string}).rawBody += chunk;
});

req.on('end', () => {
next();
});
}
33 changes: 33 additions & 0 deletions packages/dev-server-api/src/securityHeadersMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import http from 'http';

export default function securityHeadersMiddleware(
req: http.IncomingMessage,
res: http.ServerResponse,
next: (err?: any) => void,
) {
// Block any cross origin request.
if (
typeof req.headers.origin === 'string' &&
!req.headers.origin.match(/^https?:\/\/localhost:/)
) {
next(
new Error(
'Unauthorized request from ' +
req.headers.origin +
'. This may happen because of a conflicting browser extension. Please try to disable it and try again.',
),
);
return;
}

// Block MIME-type sniffing.
res.setHeader('X-Content-Type-Options', 'nosniff');

next();
}
18 changes: 18 additions & 0 deletions packages/dev-server-api/src/statusPageMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import http from 'http';

/**
* Status page so that anyone who needs to can verify that the packager is
* running on 8081 and not another program / service.
*/
export default function statusPageMiddleware(
_req: http.IncomingMessage,
res: http.ServerResponse,
) {
res.end('packager-status:running');
}
Loading