-
Notifications
You must be signed in to change notification settings - Fork 930
feat: add a programmatic dev server API #1118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" | ||
fson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }, | ||
| "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" | ||
| ] | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'); | ||
| }; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)); | ||
fson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| }); | ||
|
|
||
| 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, | ||
| }; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
35
packages/dev-server-api/src/openStackFrameInEditorMiddleware.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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'); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.